Как подогнать текст под точную ширину на холсте html? - PullRequest
0 голосов
/ 22 февраля 2020

Как мне разместить однострочную строку текста с точной шириной на холсте html5? До сих пор я пытался написать текст с начальным размером шрифта, измерить ширину текста с помощью measureText(my_text).width, а затем вычислить новый размер шрифта на основе соотношения между желаемой шириной текста и фактической шириной текста. Это дает приблизительно правильные результаты, но в зависимости от текста на краях есть пробелы.

Вот пример кода:

// Draw "guard rails" with 200px space in between
c.fillStyle = "lightgrey";
c.fillRect(90, 0, 10, 200);
c.fillRect(300, 0, 10, 200);

// Measure how wide the text would be with 100px font
var my_text = "AA";
var initial_font_size = 100;
c.font = initial_font_size + "px Arial";
var initial_text_width = c.measureText(my_text).width;

// Calculate the font size to exactly fit the desired width of 200px
var desired_text_width = 200; 
new_font_size = initial_font_size * desired_text_width / initial_text_width;

// Draw the text with the new font size
c.font = new_font_size + "px Arial";
c.fillStyle = "black";
c.textBaseline = "top";
c.fillText(my_text, 100, 0, 500);

Результат идеально подходит для некоторых строк, например "AA":

enter image description here

Но для других строк, таких как "BB", по краям есть пробел, и вы можете видеть, что текст не достигает "ограждений":

enter image description here

Как сделать так, чтобы текст всегда доходил до краев?

Ответы [ 3 ]

1 голос
/ 17 апреля 2020

У меня была похожая проблема в моем проекте. Мне нужно было не только получить точную ширину текста, но я также понял, что если бы я рендерил текст в позиции X, он иногда перетекал бы влево от X из-за Боковых подшипников .

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

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

Этот код был протестирован только в Chrome и Firefox, но должен работать практически во всех современных браузерах . Он также поддерживает использование веб-шрифтов, которые просто необходимо загрузить на страницу, а затем на них можно ссылаться по имени.

class TextMeasurer {
  constructor() {
    const SVG_NS = "http://www.w3.org/2000/svg";

    this.svg = document.createElementNS(SVG_NS, 'svg');

    this.svg.style.visibility = 'hidden';
    this.svg.setAttribute('xmlns', SVG_NS)
    this.svg.setAttribute('width', 0);
    this.svg.setAttribute('height', 0);

    this.svgtext = document.createElementNS(SVG_NS, 'text');
    this.svg.appendChild(this.svgtext);
    this.svgtext.setAttribute('x', 0);
    this.svgtext.setAttribute('y', 0);

    document.querySelector('body').appendChild(this.svg);
  }

  /**
   * Measure a single line of text, including the bounding box, inner size and lead and trail X
   * @param {string} text Single line of text
   * @param {string} fontFamily Name of font family
   * @param {string} fontSize Font size including units
   */
  measureText(text, fontFamily, fontSize) {
    this.svgtext.setAttribute('font-family', fontFamily);
    this.svgtext.setAttribute('font-size', fontSize);
    this.svgtext.textContent = text;

    let bbox = this.svgtext.getBBox();
    let textLength = this.svgtext.getComputedTextLength();

    // measure the overflow before and after the line caused by font side bearing
    // Rendering should start at X + leadX to have the edge of the text appear at X
    // when rendering left-aligned left-to-right
    let baseX = parseInt(this.svgtext.getAttribute('x'));
    let overflow = bbox.width - textLength;
    let leadX = Math.abs(baseX - bbox.x);
    let trailX = overflow - leadX;

    return {
      bbWidth: bbox.width,
      textLength: textLength,
      leadX: leadX,
      trailX: trailX,
      bbHeight: bbox.height
    };
  }
}

//Usage:
let m = new TextMeasurer();
let textDimensions = m.measureText("Hello, World!", 'serif', '12pt');
document.getElementById('output').textContent = JSON.stringify(textDimensions);
<body>
  <div id="output"></div>
</body>
0 голосов
/ 22 февраля 2020

Измерение ширины текста

Измерение текста проблематично c на многих уровнях.

Полная и экспериментальная textMetric была определена в течение многих лет, но доступна только в одном браузере основного потока (Safari), скрыта за флагами (Chrome), скрыта из-за ошибок (Firefox ), статус неизвестен (Edge, IE).

Использование только width

В лучшем случае вы можете использовать свойство width объекта, возвращаемого ctx.measureText, для оценки ширины. Эта ширина больше или равна фактической ширине пикселя (слева направо). Обратите внимание, что веб-шрифты должны быть полностью загружены, или их ширина может соответствовать ширине заполнителя шрифта.

Грубая сила

Единственный метод, обеспечивающий надежную работу швов, - это, к сожалению, метод грубой силы, который визуализирует шрифт. на временный / или рабочий холст и вычисляет экстент путем запроса пикселей.

Это будет работать во всех браузерах, поддерживающих холст.

Он не подходит для анимации и приложений в реальном времени. .

Следующая функция

  • Вернет объект со следующими свойствами

    • width ширина в пикселях холста текста
    • left расстояние слева от первого пикселя в пикселях холста
    • right расстояние слева до последнего обнаруженного пикселя в пикселях холста
    • rightOffset расстояние в пикселе холста от измеренного ширина текста и обнаруженный правый край
    • measuredWidth измеренная ширина, возвращаемая ctx.measureText
    • baseSize размер шрифта в пикселях
    • font используемый шрифт T o измерить текст
  • Он вернет undefined, если ширина равна нулю или строка не содержит видимого текста.

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

Точность зависит от размера измеряемого шрифта. Функция использует фиксированный размер шрифта 120px. Базовый размер можно установить, передав свойство

. Функция может использовать частичный текст (Short Cut) для сокращения оперативной памяти и накладных расходов на обработку. Свойство rightOffset - это расстояние в пикселях от правого края ctx.measureText до первого пикселя с содержимым.

Таким образом, вы можете измерить текст "CB" и использовать его для точного выравнивания любого текста, начиная с "C" и заканчивается "B"

Пример использования короткого текста

    const txtSize = measureText({font: "arial", text: "BB"});
    ctx.font = txtSize.font;
    const width = ctx.measureText("BabcdefghB").width;
    const actualWidth = width - txtSize.left - txtSize.rightOffset;
    const scale = canvas.width / actualWidth;
    ctx.setTransform(scale, 0, 0, scale,  -txtSize.left * scale, 0);
    ctx.fillText("BabcdefghB",0,0);

measureText function

const measureText = (() => {
    var data, w, size =  120; // for higher accuracy increase this size in pixels.
    const isColumnEmpty = x => {
       var idx = x, h = size * 2;
       while (h--) {
           if (data[idx]) { return false }
           idx += can.width;
       }
       return true;
    }
    const can = document.createElement("canvas");
    const ctx = can.getContext("2d");
    return ({text, font, baseSize = size}) => {   
        size = baseSize;
        can.height = size * 2;
        font = size + "px "+ font;          
        if (text.trim() === "") { return }
        ctx.font = font;
        can.width = (w = ctx.measureText(text).width) + 8;
        ctx.font = font;
        ctx.textBaseline = "middle";
        ctx.textAlign = "left";
        ctx.fillText(text, 0, size);
        data = new Uint32Array(ctx.getImageData(0, 0, can.width, can.height).data.buffer);
        var left, right;
        var lIdx = 0, rIdx = can.width - 1;
        while(lIdx < rIdx) {
            if (left === undefined && !isColumnEmpty(lIdx)) { left = lIdx }
            if (right === undefined && !isColumnEmpty(rIdx)) { right = rIdx }
            if (right !== undefined && left !== undefined) { break }
            lIdx += 1;
            rIdx -= 1;
        }
        data = undefined; // release RAM held
        can.width = 1; // release RAM held
        return right - left >= 1 ? {
            left, right, rightOffset: w - right,  width: right - left, 
            measuredWidth: w, font, baseSize} : undefined;
    }   
})();

Пример использования

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

Введите текст для ввода текста.

  • Если текст слишком большой, чтобы поместиться на холсте, консоль будет отображать предупреждение.
  • Если масштаб текста больше 1 (что означает, что отображаемый шрифт больше, чем измеренный шрифт), консоль отобразит предупреждение, так как могут быть некоторые потеря точности выравнивания.

inText.addEventListener("input", updateCanvasText);
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 500;

function updateCanvasText() {
    const text = inText.value.trim(); 
    const shortText = text[0] + text[text.length - 1];
    const txtSize = measureText({font: "arial", text: text.length > 1 ? shortText: text});
    if(txtSize) {
        ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height)
        ctx.font = txtSize.font;
        const width = ctx.measureText(text).width;
        const actualWidth = width - txtSize.left - txtSize.rightOffset;
        const scale =  (canvas.width - 20) / actualWidth;
        console.clear();
        if(txtSize.baseSize * scale > canvas.height) {
            console.log("Font scale too large to fit vertically");
        } else if(scale > 1) {
            console.log("Scaled > 1, can result in loss of precision ");
        }
        ctx.textBaseline = "top";
        ctx.fillStyle = "#000";
        ctx.textAlign = "left";
        ctx.setTransform(scale, 0, 0, scale, 10 - txtSize.left * scale, 0);
        ctx.fillText(text,0,0);
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.fillStyle = "#CCC8";
        ctx.fillRect(0, 0, 10, canvas.height);
        ctx.fillRect(canvas.width - 10, 0, 10, canvas.height);
    } else {
        console.clear();
        console.log("Empty string ignored");
    }
}
const measureText = (() => {
    var data, w, size =  120;
    const isColumnEmpty = x => {
       var idx = x, h = size * 2;
       while (h--) {
           if (data[idx]) { return false }
           idx += can.width;
       }
       return true;
    }
    const can = document.createElement("canvas");
    const ctx = can.getContext("2d");
    return ({text, font, baseSize = size}) => {   
        size = baseSize;
        can.height = size * 2;
        font = size + "px "+ font;          
        if (text.trim() === "") { return }
        ctx.font = font;
        can.width = (w = ctx.measureText(text).width) + 8;
        ctx.font = font;
        ctx.textBaseline = "middle";
        ctx.textAlign = "left";
        ctx.fillText(text, 0, size);
        data = new Uint32Array(ctx.getImageData(0, 0, can.width, can.height).data.buffer);
        var left, right;
        var lIdx = 0, rIdx = can.width - 1;
        while(lIdx < rIdx) {
            if (left === undefined && !isColumnEmpty(lIdx)) { left = lIdx }
            if (right === undefined && !isColumnEmpty(rIdx)) { right = rIdx }
            if (right !== undefined && left !== undefined) { break }
            lIdx += 1;
            rIdx -= 1;
        }
        data = undefined; // release RAM held
        can.width = 1; // release RAM held
        return right - left >= 1 ? {left, right, rightOffset: w - right, width: right - left, measuredWidth: w, font, baseSize} : undefined;
    }   
})();
body {
  font-family: arial;
}
canvas {
   border: 1px solid black;
   width: 500px;
   height: 500px;   
}
<label for="inText">Enter text </label><input type="text" id="inText" placeholder="Enter text..."/>
<canvas id="canvas"></canvas>

Примечание декоративные шрифты могут не работать, может потребоваться увеличить высоту холста в функции measureText

0 голосов
/ 22 февраля 2020

Проблема, с которой вы сталкиваетесь, заключается в том, что TextMetrics.width представляет " ширину продвижения " текста.
Этот ответ довольно хорошо объясняет, что и ссылки на хороших ресурсов .

Ширина продвижения - это расстояние между начальной позицией пера глифа и начальной позицией пера следующего глифа.

Здесь вам нужен ограничивающий прямоугольник width, и чтобы получить это, вам необходимо вычислить сумму TextMetric.actualBoundingBoxLeft + TextMetric.actualBoundingBoxRight.
Обратите внимание также, что при рендеринге текста вы будете приходится учитывать смещение actualBoundingBoxLeft ограничивающего прямоугольника, чтобы оно правильно помещалось.

К сожалению, все браузеры не поддерживают расширенные TextMetrics объекты и фактически только Chrome действительно так, потому что Safari ложно возвращает ширину шага для значений начального поля. Для других браузеров нам не повезло, и нам приходится полагаться на уродливые хаки getImageData.

const supportExtendedMetrics = 'actualBoundingBoxRight' in TextMetrics.prototype;
if( !supportExtendedMetrics ) {
  console.warn( "Your browser doesn't support extended properties of TextMetrics." );
}

const canvas = document.getElementById('canvas');
const c = canvas.getContext('2d');
c.textBaseline = "top";

const input = document.getElementById('inp');
input.oninput = (e) => {

  c.clearRect(0,0, canvas.width, canvas.height);
  // Draw "guard rails" with 200px space in between
  c.fillStyle = "lightgrey";
  c.fillRect(90, 0, 10, 200);
  c.fillRect(300, 0, 10, 200);

  c.fillStyle = "black";
  fillFittedText(c, inp.value, 100, 0, 200) ;

};
input.oninput();

function fillFittedText( ctx, text = "", x = 0, y = 0, target_width = ctx.canvas.width, font_family = "Arial" ) {
  let font_size = 1;
  const updateFont = () => {
    ctx.font = font_size + "px " + font_family;
  };
  updateFont();
  let width = getBBOxWidth(text);
  // first pass width increment = 1
  while( width && width <= target_width ) {
    font_size++;
    updateFont();
    width = getBBOxWidth(text);
  }
  // second pass, the other way around, with increment = -0.1
  while( width && width > target_width ) {
    font_size -= 0.1;
    updateFont();
    width = getBBOxWidth(text);
  }
  // revert to last valid step
  font_size += 0.1;
  updateFont();
  
  // we need to measure where our bounding box actually starts
  const offset_left = c.measureText(text).actualBoundingBoxLeft || 0;
  ctx.fillText(text, x + offset_left, y);

  function getBBOxWidth(text) {
    const measure = ctx.measureText(text);
    return supportExtendedMetrics ? 
      (measure.actualBoundingBoxLeft + measure.actualBoundingBoxRight) :
      measure.width;
  }

}
<input type="text" id="inp" value="BB">
<canvas id="canvas" width="500"></canvas>
...