Javascript/Jquery making text to fit perfectly on div given dimensions with line break(if needed)

前端 未结 1 1336
迷失自我
迷失自我 2020-12-04 03:33

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 getFon         


        
相关标签:
1条回答
  • 2020-12-04 03:59

    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.

    function getBestFontSize(text, width, height, userstyles) {
      if(!text) return null;
      
      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 before
          break;
        }
        size++;
      }
      if(size === 0) {
        // even at 0 it doesn't fit...
        return null;
      }
      // 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);
      // cleanup
      document.body.removeChild(cont);
    
      return {
        fontSize: size,
        lines: lines
      };
    }
    
    function getLineBreaks(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;
    }
    <textarea id="txt_area">This is an example text to fit the div even with line breaks</textarea>
    <canvas id="canvas"></canvas>

    0 讨论(0)
提交回复
热议问题