マガジン

文字の折り返し:どこで改行するかを決める処理

遊びをつくる

執筆にClaudeを使用しています。

活版のような抽象的な字形とローラーを並べ、太さや間隔を検討する写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

ゲームの台詞ウィンドウに、同じ意味の英文と日本語文を同じ幅で入れる場面を考えてみます。英文は単語の切れ目で行が変わり、行末に少し余白が残ります。日本語文は余白がほとんど出ず、文字の途中でも行が変わります。ところが日本語側で「。」や「」」が行の先頭に落ちると、途端に読みにくくなります。 この差は、折り返しの処理が「どこで切ってよいか」という規則を言語ごとに持っているために生まれます。この記事では、折り返しの基本アルゴリズムを短いコードで組み、英語向けの規則と日本語向けの規則を同じ画面に並べて比べます。具体的な出力はここには書きません。コードを動かし、幅を変えながら自分の目で確かめる形にしています。

この記事の目次
  1. 折り返しとは「次の文字が入るか」を測り続けること
  2. 英語は空白、日本語は文字ごと:規則が違う理由
  3. 画面で比べる:三列の最小コード
  4. 結果の読み方と起こりやすい誤り
  5. 公式作品で確かめる「設計から読み取れること」
  6. 次の行動
  7. 参考資料

折り返しとは「次の文字が入るか」を測り続けること

折り返しの核は単純です。文を小さな部品(トークン)に分け、1つずつ現在の行へ足します。足したあとの幅が行の最大幅を超えるなら、足す前に改行します。

  1. 文をトークンに分けます。
  2. 現在の行にトークンを足した場合の幅を測ります。
  3. 最大幅を超えるなら、現在の行を確定して次の行を始めます。
  4. 超えないなら、そのまま足します。
  5. 最後に残った行を確定します。

ここで決まるのは「トークンをどう作るか」と「超えたときに例外を許すか」の2点だけです。言語ごとの違いは、この2点に集まっています。

幅は文字数で数えず、実際に描画する幅で測ります。ブラウザのCanvasなら measureText が使えます。英字は細い文字と太い文字で幅が違い、日本語の全角文字はほぼ同じ幅です。文字数で数えると、英語側の行が不揃いになります。

英語は空白、日本語は文字ごと:規則が違う理由

英語は単語の間に空白があります。切ってよい場所は空白の後ろが基本なので、トークンは「単語+後ろの空白」にします。単語の途中で切ると読めなくなるため、1語が行の幅より長いときだけ例外が必要になります。

日本語には単語間の空白がありません。ほとんどの文字の間で切れるので、トークンは1文字ずつです。一方で、行頭に置いてはいけない文字(行頭禁則)や、行末に置いてはいけない文字(行末禁則)があります。

  • 切れ目の候補、英語:空白の後ろ、日本語:ほとんどの文字の間
  • トークンの単位、英語:単語+空白、日本語:1文字
  • 避けたい位置、英語:単語の途中、日本語:行頭の「、。」や閉じ括弧、行末の開き括弧
  • 例外の処理、英語:長すぎる語を強制的に切る、日本語:禁則文字を前の行に残す(ぶら下げ)か、直前の文字ごと送る

こうした規則は、Unicodeの改行規則(UAX #14)や日本語組版の要件に整理されています。CSSにも word-break、overflow-wrap、line-break といったプロパティがあります。ただし、それぞれの細かい挙動はブラウザの実装や仕様の版で確認が必要です。この記事の自作コードは、規則の考え方をつかむための簡易版です。

画面で比べる:三列の最小コード

次のHTMLは、1ファイルで動きます。同じ幅の箱を3つ並べます。

  • 左:英文を空白で切る
  • 中:日本語文を、英語と同じ「空白で切る」方式で処理する
  • 右:日本語文を、文字ごとに切る

スライダーで幅を変え、チェックボックスで行頭禁則のオン・オフを切り替えます。

html
<!doctype html>
<meta charset="utf-8">
<style>canvas{border:1px solid #888;display:block}</style>
<label>幅 <input id="w" type="range" min="80" max="200" value="140"></label>
<label><input id="k" type="checkbox" checked> 行頭禁則</label>
<canvas id="c" width="660" height="300"></canvas>
<script>
const NG=new Set("、。,.」』)!?ー");
const JA="手紙を集めて、赤い郵便受けへ向かいます。「急ぎましょう」";
const T=[["英語・空白で切る","The quick brown fox jumps over the lazy dog.","word"],
["日本語・空白で切る",JA,"word"],["日本語・文字で切る",JA,"char"]];
const c=document.getElementById("c"),g=c.getContext("2d");
function wrap(t,max,mode,kin){
  const tk=mode=="word"?t.match(/\S+\s*/g):[...t];
  const out=[];let line="";
  for(const s of tk){
    const over=line&&g.measureText((line+s).trimEnd()).width>max;
    if(!over){line+=s;continue;}
    if(kin&&mode=="char"&&NG.has(s)){line+=s;continue;}
    out.push(line);line=s;
  }
  if(line)out.push(line);
  return out;
}
function draw(){
  const max=+w.value,kin=k.checked;
  g.clearRect(0,0,c.width,c.height);
  g.font="16px sans-serif";g.textBaseline="top";
  T.forEach(([label,text,mode],i)=>{
    const x=10+i*220;
    g.fillText(label,x,2);g.strokeRect(x,24,max,260);
    wrap(text,max,mode,kin).forEach((l,j)=>g.fillText(l,x,28+j*22));
  });
}
w.oninput=k.onchange=draw;draw();
</script>

処理の要点は wrap 関数の3か所です。

  • トークンの作り方:word では正規表現 \S+\s*(空白以外の連続と、後ろの空白)で、char では1文字ずつ切ります。
  • 幅の判定:行末の空白は見た目の幅に含めないため、trimEnd() してから測ります。
  • ぶら下げ:char モードで行頭禁則がオンのとき、次に来るトークンが禁則文字なら、幅を超えていても現在の行に足します。

このコードの構造から言えることがあります。空白のない日本語文を word 方式にかけると、文全体が1つのトークンになります。行が空のうちは「超えたら改行」の判定が働かないので、その1行は箱の外へはみ出すはずです。ただし、実際にどう見えるかは自分の環境で確かめてください。

この簡易版は、禁則文字が連続する場合、行末禁則(開き括弧「「」など)、英語の長すぎる単語の強制分割を扱っていません。実際のゲームに入れるなら、ここから足すことになります。

単語単位で切る方法もある

日本語を文字ごとではなく、語のまとまりで切りたい場合は、ブラウザ標準の Intl.Segmenter が使えます。

js
const seg=new Intl.Segmenter("ja",{granularity:"word"});
const tk=[...seg.segment(JA)].map(s=>s.segment);

この tk を wrap の char 側のトークンとして使うと、語の途中で切れにくくなるはずです。ただし、語の区切りが環境(ブラウザの版やOS)によって違うことがあります。切れ目が期待どおりか、使う環境で確かめてください。

結果の読み方と起こりやすい誤り

幅を変えながら、次の手順で記録します。

  1. 幅を80・110・140・170・200の5段階にします。
  2. 各幅で、3つの箱の行数を数えます。
  3. 右の箱で、行頭に「、」「。」「」」が来ていないか見ます。行頭禁則をオン・オフ両方で確認します。
  4. 箱の右端からはみ出している行があるか見ます。

読み方の目安は次のとおりです。

  • 中の箱だけがはみ出す:空白で切る方式は、空白のない文に使えません。日本語のテキストは文字単位か語単位で切る必要があります。
  • 禁則オフで行頭に句読点が出る:禁則処理は見た目の品質に直結します。台詞の末尾が「。」だけで次の行に落ちるのは、読み手に目立ちます。
  • 禁則オンで右端が箱をわずかに超える:ぶら下げの副作用です。許容するか、1文字前で改行するか、設計で決めます。

起こりやすい誤りもあります。

  • measureText を呼ぶ前にフォントを設定していないと、違うフォントで測ってしまいます。
  • Webフォントが読み込まれる前に測ると、描画後に幅がずれます。
  • 半角と全角が混ざる文では、文字数で数えた幅の見積もりが合いません。
  • 画面の拡大率や高解像度の設定を変えても、論理的な幅の比較は同じになるよう、測定と描画は同じ設定で行います。

公式作品で確かめる「設計から読み取れること」

アソバースの13の公式作品について、折り返しの処理方法そのものは確認済みの事項にありません。したがって、どの作品がどんな折り返しを実装しているかは、ここでは述べません。ただ、確認済みの仕様から、折り返しを意識せざるを得ない場面は読み取れます。

  • 日本語と英語の表示切り替え:13作品はサイトの言語設定に合わせて表示を切り替えます。同じ画面でも文字列の長さと切れ目の規則が変わるため、どちらの言語でも収まる設計が要ります。作者が入力した作品名や紹介文は書き換えないので、日英が混ざる画面も起こりえます。
  • プレイ画面の拡大:拡大しても進行は保たれます。表示幅が変われば、折り返し位置も変わるはずです。拡大したときに文字がはみ出さないか、作品を開いて確かめます。
  • WAVE / WAVE:狭い画面ではステップが横スクロールになります。折り返さず、スクロールを選ぶ設計です。折り返せない種類の表示があることを示す例です。
  • 灯りのチェス:取った駒と全棋譜を表示します。棋譜は対局が進むほど長くなります。長いテキストが画面の幅でどう扱われるかは、対局を進めて確かめます。

確かめるときは、言語を切り替え、画面の幅を変え、文字量の多い画面(棋譜、図鑑、観察ノートなど)を開いて、行頭の記号と枠からのはみ出しを見ます。結果は作品ごとに違うため、ここでは書きません。

次の行動

上のHTMLを保存してブラウザで開き、幅を5段階に変えて、3つの箱の行数と行頭の文字を記録してください。そのあと、自作ゲームの文字表示でも、日本語と英語で同じ確認をしてみてください。

参考資料

  • Unicode Standard Annex #14 Unicode Line Breaking Algorithm
  • 日本語組版処理の要件(W3C)
  • CSS Text Module(W3C)
  • MDN Web Docs「CanvasRenderingContext2D.measureText()」
  • MDN Web Docs「Intl.Segmenter」