やりたいのは 1文字だけの改行の拒否 - Hail2u のようなことの延長です。長めの見出しがブラウザによって改行されると、どうもバランスが悪くなったり可読性が微妙になることが多い。これをなんとかする。

仕様

  • 元の高さから変わらないこと
    • 非同期にやってもガタガタしないこと
  • 各行ができるだけバランスをとること
    • 文字数 (正確には幅) をあわせる

元の高さから変わらないことというのは行数を変えないということです。これを制約にしてページ全体の高さに影響を与えないようにすることで、非同期的に行数を調整しても閲覧に支障がないようにという意図があります。

「各行のバランス」はできるだけ行ごとの幅を揃えるという意味でいっています。基本的に幅が揃うことはないので、あくまでできるだけです。また、揃わない場合には一番下の行が一番長くなるようにします。これはデザイン上、重心が下になるほうが安定して見えるからです。

実装

  • 改行しても良い単位ごとに文を分割する
  • 各セグメントの文字幅を計算し、各行に埋めていく

デモ

また、このサイトにも適用済みです。

文の分割

いわゆる形態素解析での単語単位の「わかち書き」だと分割されすぎてしまいます。基本的に文節単位で改行するのが適切ではないかと思うので、文節単位のわかち書き機みたいなのが欲しくなります。

そこで TinySegmenterMaker を使ってみました。TinySegmenter を任意の学習データから生成できる優れものです。TinySegmenter 自身は言語非依存のアルゴリズムのため、一般的な形態素解析の分割位置とは違う分割でも学習さえさせれば動いてくれそうです。

適当なコーパスを用意できなかったため、とりあえず自分で書いた日記 (これ) の過去ログを全て MeCab で解析し、副詞などを結合する処理を加えたあとにスペース区切りで出力し、TinySegmenterMaker の入力としました。学習データ的に汎用性は落ちますが、そもそも自分のサイト用なのでまぁいい気もします。

そんなに元データは多くありませんが結構時間がかかりました。

なお分割時の MeCab 辞書に mecab-ipadic-neologd も入れてます。不必要な分割が減ることを期待しています。

各行に埋める

1行に収まっている場合は処理しません。

場合によって全く改行位置を調整できないケースも生じます。つまり各行に文字がいっぱいっぱいに詰まっている場合、調整できません。この場合も諦めてブラウザにまかせます。ただ、最後のセグメントには改行禁止ゼロスペース文字を入れることで1文字だけ残るというのは可能な限り避けます。

文字幅の計算には canvas の measureText を使っています。カーニングやリガチャなどが適切に反映されない可能性がありますが、現時点だとこれ以上良い方法がない気がします。

Webフォントを使う場合、ロードされていることを実行前に保証する必要があります。document.fonts.ready がちゃんと使えればいいんですが、Safari の挙動がおかしいので使えませんでした。

そこで以下のようにしてWebフォントのロードを判定しています。

function webfontReady (font, opts) {
	if (!opts) opts = {};
	return new Promise(function (resolve, reject) {
		var canvas = document.createElement('canvas');
		var ctx = canvas.getContext('2d');
		var TEST_TEXT = "test.@01N日本語";
		var TEST_SIZE = "100px";


		var timeout = Date.now() + (opts.timeout || 3000);
		(function me () {
			ctx.font = TEST_SIZE + " '" + font + "', sans-serif";
			var w1 = ctx.measureText(TEST_TEXT).width;
			ctx.font = TEST_SIZE + " '" + font + "', serif";
			var w2 = ctx.measureText(TEST_TEXT).width;
			ctx.font = TEST_SIZE + " '" + font + "', monospace";
			var w3 = ctx.measureText(TEST_TEXT).width;
			console.log(w1, w2, w3);
			if (w1 === w2 && w1 === w3) {
				resolve();
			} else {
				if (Date.now() < timeout) {
					setTimeout(me, 100);
				} else {
					reject('timeout');
				}
			}
		})();
	});
}

もっと良くできるか?

本当は各形態素境界ごとにスコアリングして、読みやすい順に改行を加えるみたいなことができればいいんですが、僕の技術力だとむずかしそうです。クライアント幅によるという性質上、処理はクライアントサイドでやる必要がありますが、そうすると実行ファイルサイズも問題になってきます。

そもそも学習データとして「適切な改行位置」を与えるのが難しい問題があります。Cabocha を使えばもうちょっとマシになるでしょうか?

「下のほうを長くする」という方針がいまいちだと思います。意味的に区切れるところを優先して区切るのが正しいと思われます。

備考: TinySegmenterMaker のマルチスレッドバージョン

なんかどうも boost_system も必要でした。以下のようにコンパイルしました。

g++ -I/usr/local/include -L/usr/local/lib -DMULTITHREAD -lboost_thread-mt -lboost_system -O3 -o train train.cpp

あと train に引数を与えないとマルチスレッドで処理されませんでした。8スレッドでやるなら -m 8 を加えます。

./extract < /tmp/corpus.txt > features.txt
./train -t 0.001 -n 10000 -m 8 features.txt model
./maker javascript < model

レポジトリ

https://github.com/cho45/midashi-kaigyo あまり整理されてません。

追記: budou

ブコメ で教えてもらいましたが、Google がまさに同じことをやってました

Google の NL APIを使っているみたいです。

追記

文字を変形させるという発想がなかったのですが、編集系の識者のかたから長体かけつつ字送り詰めて押し込んだりしますという意見をいただきました。また、これを実現する方法として CSS transform を使えばいいのではないかという意見もいただきました。

  1. トップ
  2. tech
  3. 見出しの改行位置を適正化する試み