Extensions

Source: utils/layoutText.js

const _layoutPart = (lines, ctx, chars, maxWidth) => {
  let charLine = "";
  for (let c = 0; c < chars.length; c++) {
    const testChar = charLine + chars[c];
    if (ctx.measureText(testChar).width <= maxWidth) charLine = testChar;
    else {
      charLine && lines.push(charLine);
      charLine = chars[c];
    }
  }
  charLine && lines.push(charLine);
};

/**
 * Layouts text into lines based on the maximum width.
 *
 * @param {CanvasRenderingContext2D} ctx - The canvas rendering context used to measure text width.
 * @param {string} text - The text to be laid out.
 * @param {number} maxWidth - The maximum width for each line.
 * @returns {string[]} An array of strings, each representing a line of laid-out text.
 */
export const layoutText = (ctx, text, maxWidth) => {
  const lines = [],
    textLines = text.split("\n");

  for (let i = 0; i < textLines.length; i++) {
    const paragraph = textLines[i];

    if (paragraph.trim() === "") {
      lines.push("");
      continue;
    }

    let currentLine = "",
      cursor = 0;

    while (cursor < paragraph.length) {
      let nextSpace = paragraph.indexOf(" ", cursor);
      if (nextSpace === -1) nextSpace = paragraph.length;

      const word = paragraph.slice(cursor, nextSpace),
        testLine = currentLine + (currentLine === "" ? "" : " ") + word;

      if (ctx.measureText(testLine).width <= maxWidth) currentLine = testLine;
      else {
        if (ctx.measureText(word).width > maxWidth) {
          if (currentLine) {
            lines.push(currentLine);
            currentLine = "";
          }

          if (word.includes("-")) {
            const parts = word.split(/(-)/);
            let partLine = "";

            for (let p = 0; p < parts.length; p++) {
              const testPart = partLine + parts[p];
              if (ctx.measureText(testPart).width <= maxWidth)
                partLine = testPart;
              else {
                partLine && lines.push(partLine);

                if (ctx.measureText(parts[p]).width > maxWidth) {
                  _layoutPart(lines, ctx, [...parts[p]], maxWidth);
                  partLine = "";
                } else partLine = parts[p];
              }
            }
            partLine && lines.push(partLine);
          } else _layoutPart(lines, ctx, [...word], maxWidth);
        } else {
          currentLine && lines.push(currentLine);
          currentLine = word;
        }
      }

      cursor = nextSpace + 1;
    }

    currentLine && lines.push(currentLine);
  }

  return lines;
};