Skip to content Skip to sidebar Skip to footer

Javascript/jquery Making Text To Fit Perfectly On Div Given Dimensions With Line Break(if Needed)

I'm having a little hard time working this out: I have these functions that I use in order to fit a single lined text into given dimensions. function getFontSize(width, height, tex

Solution 1:

The easiest is probably to use the power of CSS instead of trying to do it yourself.

You can create a dummy div, append a <span> into it, and increase its font-size until it doesn't fit anymore. The font-size before was the correct one.

Then, you can walk over this <span>'s textContent using a Range object in order to get the line-breaks CSS will have generated. The Range API provides a convenient getBoundingClientRect method, allowing us to find where the cursor is. We just have to remember the last y position and when it changes, we know the character before was the last of the line.

There are some downsides with this technique though.

  • In the DOM multiple space characters are compressed in a single one. CanvasContext API doesn't have the same behavior, so we need to get rid of these before parsing the text.

  • The measures are made based on the containers bounding boxes, this means it doesn't check for the painted pixels, so you may have some characters overflowing (for instance Zalgo͚̠͓ͣ̔͐̽) and some others not fitting perfectly in the box (depending on the ascent and descents of each characters).

  • ... probably others.

functiongetBestFontSize(text, width, height, userstyles) {
  if(!text) returnnull;
  
  const cont = document.createElement('div');
  cont.classList.add('best-font-size-tester');
  const style = cont.style;

  if(typeof width === 'number') width += 'px';
  if(typeof height === 'number') height += 'px';
  Object.assign(style, {width, height}, userstyles);

  const span = document.createElement('span');
  span.textContent = text;
  cont.appendChild(span);
  document.body.appendChild(cont);
  
  let size = 0;
  
  const max = cont.getBoundingClientRect();
  while(true) {
    style.fontSize = size + 'px';
    let rect = span.getBoundingClientRect();
    if(rect.bottom > max.bottom || rect.right > max.right) {
      // overflown
      size -= 1; // the correct size was the one beforebreak;
    }
    size++;
  }
  if(size === 0) {
    // even at 0 it doesn't fit...returnnull;
  }
  // now we'll get the line breaks by walking through our text content
  style.fontSize = size + 'px';
  const lines = getLineBreaks(span.childNodes[0], max.top);
  // cleanupdocument.body.removeChild(cont);

  return {
    fontSize: size,
    lines: lines
  };
}

functiongetLineBreaks(node, contTop) {
  if(!node) return [];
  const range = document.createRange();
  const lines = [];
  range.setStart(node, 0);
  let prevBottom = range.getBoundingClientRect().bottom;
  let str = node.textContent;
  let current = 1;
  let lastFound = 0;
  let bottom = 0;
  while(current <= str.length) {
    range.setStart(node, current);
    if(current < str.length -1)
      range.setEnd(node, current+1);
    bottom = range.getBoundingClientRect().bottom;
    if(bottom > prevBottom) {
      lines.push({
        y: prevBottom - (contTop || 0),
        text: str.substr(lastFound , current - lastFound)
      });
      prevBottom = bottom;
      lastFound = current;
    }
    current++;
  }
  // push the last line
  lines.push({
    y: bottom - (contTop || 0),
    text: str.substr(lastFound)
  });

  return lines;
}

const ctx = canvas.getContext('2d');
ctx.textBaseline = 'bottom';
txt_area.oninput = e => {
  const input = txt_area.value
    .replace(/(\s)(?=\1)/g, ''); // remove all double spaces
  ctx.setTransform(1,0,0,1,0,0);
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.translate(19.5,19.5);
  ctx.strokeRect(0,0,100,100);

  if(!input.length) return;
  const bestFit = getBestFontSize(input, 100, 100, {
    fontFamily: 'sans-serif',
    fontWeight: '600',
    textAlign: 'center'
  });
  // apply thesame options we passed
  ctx.font = '600 ' + bestFit.fontSize + 'px sans-serif';
  ctx.textAlign = 'center';
  // translate again because text-align: center
  ctx.translate(50.5,0);
  bestFit.lines.forEach(({text, y}) => ctx.fillText(text, 0, y));
};
txt_area.oninput();
.best-font-size-tester {
  border: 1px solid;
  position: absolute;
  overflow: visible;
  opacity: 0;
  z-index: -1;
  pointer-events: none;
}
<textareaid="txt_area">This is an example text to fit the div even with line breaks</textarea><canvasid="canvas"></canvas>

Post a Comment for "Javascript/jquery Making Text To Fit Perfectly On Div Given Dimensions With Line Break(if Needed)"