2020年8月7日金曜日

JavaScriptで画面内や要素内に文字列を最大化

小さなWebアプリやWebゲームを作っていると、画面内や要素内に文字を最大化する処理は、必ずと言っていいほど出てきます。 よく使う割には作り方が知られていないので、まとめておきます。 ライブラリもいくつかあるのですが、使い勝手が悪いので自作です。

方法としては、まず表示したい文字列を適当なフォントサイズで canvas でレンダリングしてみて、仮の width/height を取得します。 そしてその width/height が表示したい要素内で最大になるようフォントサイズを指定し直すだけです。

const tmpCanvas = document.createElement('canvas');

function resizeFontSize(node) {
  // https://stackoverflow.com/questions/118241/
  function getTextWidth(text, font) {
      // re-use canvas object for better performance
      // var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
      var context = tmpCanvas.getContext("2d");
      context.font = font;
      var metrics = context.measureText(text);
      return metrics.width;
  }
  function getTextRect(text, fontSize, font, lineHeight) {
    var lines = text.split('\n');
    var maxWidth = 0;
    var fontConfig = fontSize + 'px ' + font;
    for (var i=0; i<lines.length; i++) {
      var width = getTextWidth(lines[i], fontConfig);
      if (maxWidth < width) {
        maxWidth = width;
      }
    }
    return [maxWidth, fontSize * lines.length * lineHeight];
  }
  function getNodeRect() {
    var headerHeight = document.getElementById('header').clientHeight;
    var timerHeader = document.getElementById('timerHeader');
    var timerFooterHeight = document.getElementById('timerFooter').clientHeight;
    var height = document.documentElement.clientHeight - headerHeight - timerHeader.clientHeight - timerFooterHeight;
    return [timerHeader.clientWidth, height];
  }
  function getPaddingRect(style) {
    var width = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
    var height = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
    return [width, height];
  }
  var style = getComputedStyle(node);
  var font = style.fontFamily;
  var fontSize = parseFloat(style.fontSize);
  var lineHeight = parseFloat(style.lineHeight) / fontSize;
  var nodeRect = getNodeRect();
  var textRect = getTextRect(node.innerText, fontSize, font, lineHeight);
  var paddingRect = getPaddingRect(style);

  // https://stackoverflow.com/questions/46653569/
  // Safariで正確な算出ができないので誤差ぶんだけ縮小化 (10%)
  var rowFontSize = fontSize * (nodeRect[0] - paddingRect[0]) / textRect[0] * 0.90;
  var colFontSize = fontSize * (nodeRect[1] - paddingRect[1]) / textRect[1] * 0.90;
  if (colFontSize < rowFontSize) {
    node.style.fontSize = colFontSize + 'px';
  } else {
    node.style.fontSize = rowFontSize + 'px';
  }
}
おおまかな処理としては冒頭で述べたようにシンプルですが、実用上は margin/padding をよく考えないといけません。 今回は かんたんタイマー で使っている、表示要素の width/height 算出関数を例として挙げます。 ポイントとしては、要素の縦横幅は clientWidth / clientHeight で算出できます。 文字を表示する時にはさらに padding を差し引く必要がありますが、このサンプルでは getPaddingRect() で汎用的に処理しています。

汎用的に書くと getComputedStyle() を呼び出すことになるので、処理は遅くなります。 1回しか呼ばないので気にするほどではないかもですが、事前算出してベタ書きしても良いかと思います。 例えば Bootstrap の場合、padding は画面サイズに関係なく rem 固定です。 以下のように rem → px に変換した上で、事前に padding を算出することができます。
var remSize = parseInt(getComputedStyle(document.documentElement).fontSize);


最後に補足ですが、canvas を使わない方法もあります。 ただ少しコードが長くなります。 またどちらの手法を使っても、仮の width/height を算出する内部関数 getTextWidth() は、Safari の精度がよろしくありません。 要素内ギリギリに最大化して表示しようとすると誤差ではみ出てしまう問題があるので、算出されたフォントサイズの 90% を指定すると、今のところ良い塩梅です。

0 件のコメント: