カーニングを維持したまま1文字ずつinline-blockのspanタグで区切る

1文字ずつアニメーションをかけたい時などは文字それぞれを<span>タグで区切り、display: inline-blocktransformプロパティなどを有効にします。ただし、display: inline-blockを設定する関係でそれぞれのタグの文字幅がきっちり1文字分発生するため、カーニングが効かない状態になります。

特に欧文ではその差が目立ちやすいです。

カーニングのありなしの差。AとVの間がカーニングがある方が詰まっている。

このカーニングを維持しながら<span>タグに区切る方法です。

JavaScriptを使って幅を設定する

この問題のポイントは1文字分幅が設定されてしまうためなので、カーニング済みの幅を動的に設定してあげれば回避できます。以下のようなJavaScriptコードで実現します。

JavaScript

/**
 * 1文字ずつ分解しカーニングも考慮した幅でspanタグを挿入します。
 * @param {HTMLElement} element
 * @param {string} className
 * @param {boolean} addIndex
 */
export const splitTextWithKerning = async (
  element,
  className = "",
  addIndex = false,
) => {
  const { fontStyle, fontWeight, fontSize, lineHeight, fontFamily } =
    getComputedStyle(element);
  const text = element.textContent.trim();

  // キャンバスを作成
  const canvas = document.createElement("canvas");
  const context = canvas.getContext("2d");

  const fontStyleValue = `${fontStyle} ${fontWeight} ${fontSize}/${lineHeight} ${fontFamily}`;

  // フォントのロードを待つ
  await document.fonts.load(fontStyleValue);

  // フォント設定を適用
  context.font = fontStyleValue;

  // 各文字の幅を計算
  const widths = [];
  for (let i = 0; i < text.length; i++) {
    const currentChar = text[i];
    const nextChar = text[i + 1] || "";

    // 現在の文字単体の幅
    const singleWidth = context.measureText(currentChar).width;
    // 現在の文字 + 次の文字ペアの幅
    const pairWidth = nextChar
      ? context.measureText(currentChar + nextChar).width
      : singleWidth;
    // カーニングが考慮された幅(次の文字とのペアから次の文字単体を引く)
    const kernedWidth = nextChar
      ? pairWidth - context.measureText(nextChar).width
      : singleWidth;

    widths.push(kernedWidth);
  }

  // 1文字ずつspanタグで区切って、幅を挿入する。index付与フラグがある場合はつける
  const insertHtml = [...text]
    .map(
      (char, index) =>
        `<span style="display:inline-block;width:${widths[index]}px;${addIndex ? "--index:" + index : ""}" ${className ? 'class="' + className + '"' : ""}>${char}</span>`,
    )
    .join("");

  // 要素に挿入
  element.textContent = "";
  element.insertAdjacentHTML("afterbegin", insertHtml);
};

どんなことをやっているかというと、対象の要素のテキストをcanvasに転写します。それをmeasureText()メソッドで2文字での幅を取得した後、2文字目の幅を差し引いてカーニング済みの幅を得ています。この幅を<span>タグに適用することでカーニングを維持したdisplay: inline-blockを実現しています。

なお、1文字ずつバラしたときにはインデックスを使ってCSSアニメーションを行いたい場合もあるので、--indexCSS変数の付与フラグもあります。ほかにも各<span>タグにつけたいクラス名も引数に渡せます。

注意点

1文字ずつ力技で幅を算出していくので長い文章などに実行するとパフォーマンスに影響があるかもしれません。また、Canvasに転写する関係で実際のinlineでのカーニングと少し差があります。

Webフォントを利用している場合などはフォントのロードを待つ必要があり、非同期処理となっています。そのため読み込みのタイミングなどに気をつける必要はあります。

ほかにもletter-spacingfont-feature-settigsなどは考慮されていないので、分割したあとにうまく調整してください。この関数の特性ではないですが、insertAdjacentHTML()でHTMLを挿入するのでエスケープされていないユーザー入力値などに用いないでください。セキュリティ上のリスクがあります。

Copyright Web Motion Catalog all rights reserved.