こんにちは。カートン買いでライターが溜まっているライターの内藤です。
先日、とある案件で「pタグの部分、両端揃えした方がきれいに見えない?」という話が出て、FirefoxとIE10は問題なかったのですが、WebKit系(ChromeとSafari)が対応していないことに今更気づき、何とかできないものかと思ったのがこの記事の経緯です。
具体的には、
- text-align: justify;
- text-justify: inter-ideograph;(IE用)
がWebKit系だと綺麗に見えないぜ!ということです。
目次
ブラウザによる両端揃えの見え方違い
まずは比較して見てみましょう。
Firefox
p { width: 455px; text-align: justify; }
問題なし!素敵!
IE10
p { width: 455px; text-justify: inter-ideograph; /* IE用 */ }
いつも悩ませてくれるIEも、実はこの問題には軽く対応してくれています!
しかも、フォントのアンチエイリアスも効いていてきれいなんです。すげーよIE10。Chrome
p { width: 455px; text-align: justify; }
Σ(゚д゚lll)ガーン。
ガッタガタです。同じChromeでも文字種によって揃い方が違う
WebKit系でも、text-align: justify;は効いています。
というわけで問題点は、全角・半角が混じった際、WebKit系のChromeやSafariは両端揃えをしてくれない!ということです。(違っていたらご指摘ください。)
参考になったjQueryコード
何とかならないものかと調査したところ、以下のものを見つけました。
FitText
主にレスポンシブサイトの見出しを両端揃えにするためのjQueryです。
要するに、両端揃えと言っても、文字間のスペースだけではなくて文字の大きさで調整してしまおうという代物です。下記リンクに日本語の解説があります。
jQuery ページや要素の幅に合わせて文字サイズを変更してくれるプラグイン - Stronghold:
http://zxcvbnmnbvcxz.com/jquerytips-text-fitplugin/WordPress用にslabTextがプラグイン化されていた
https://wordpress.org/plugins/wp-slabtext/
これは、「||」という区切り文字列を入れることによって、日本語が入っても意図した場所で改行できるというものです。
※2014年11月現在、2年間アップロードがされていません。
試してみた
slabTextをそのまま試してみました。
見だしタグはきれいになりましたが、pタグに入れた長い文章は全然ダメでした。研究してみた
むーん。slabTextだけではだめそうなので、日本語用にカスタマイズされたWP slabTextのソース(というより、開発者の方のページのソース)を解析してみると、「改行用の文字列を見つけたら、そこまでの文章をspanで囲っている」というものでした。
<h1 class="entry-title"> <span class="slabtext wpstxt-1">slabTextに</span> <span class="slabtext wpstxt-2">ひと手間加えて</span> <span class="slabtext wpstxt-3">日本語でも</span> <span class="slabtext wpstxt-4">意図した位置で改行してきれいに</span> <span class="slabtext wpstxt-5">両端揃えにする</span> </h1>
次に、slabTextのソースコードを読み解くと、なんかものすごくナイスアイディアが入っていました。
// Calculates the pixel equivalent of 1em within the current header var grabPixelFontSize = function() { var dummy = jQuery('<div style="display:none;font-size:1em;margin:0;padding:0;height:auto;line-height:1;border:0;"> </div>').appendTo($this), emH = dummy.height(); dummy.remove(); return emH; };
これ、すげー頭いい!ダミーで半角スペースの表示を作り、その高さを測ってダミーを消してから、その高さを返す関数なんですが、すげー使えそう!
(え?普通なんですか?)自作の幅調整jQueryとその解説
そもそも見出しじゃないし、文字サイズはそのままにしたいし……というわけで、仕方ないので自作するのでした……。
プロトタイプ1
流れは以下の通り。
- 親要素の幅を文字サイズで割って、1行に入る仮の文字数を出す。
- 親要素の幅を文字数で割って、仮の字間(letter-spacing)を出す。
- 文章を1行ずつに分割。
- 1行の中の文字を1文字ずつ変数に入れいき、
- 仮の文字数に達したら、禁則処理を行い、
- 最終的な文字数に合うように字間を調整して、
- spanでletter-spacingを設定して出力していく。
/*! jQuery slabtext plugin v2 MIT/GPL2 @freqdec */ (function( $ ){ $.fn.adaptText = function(options) { return this.each(function(){ var $this = $(this), words = $(this).text().split("\n"), origFontSize = $("body").css("font-size").replace(/px/g, ""), idealCharPerLine = null, fontRatio; // 1emの全角スペースの幅を測る function grabPixelFontSize() { var dummy = '<div id="dummy" style="display:inline-block;font-size:' + $this.css("font-size") + ';margin:0;padding:0;"> </div>'; $this.append(dummy); emW = $("#dummy").width(); $("#dummy").remove(); return emW; }; // 現在の文章の幅を測る function grabPixelLineSize(text, ratio) { var dummy = '<p id="dummy" style="-webkit-margin-before:0; -webkit-margin-after:0; margin:0; padding:0;display: inline-block;"><span style="letter-spacing: ' + ratio + 'em !important">' + text + '</span></p>'; $("body").append(dummy); emW = $("#dummy").width(); $("#dummy").remove(); return emW; }; // 親要素の禁則処理を無効にする $this.css("word-break", "break-all"); // 親要素のpaddingも無効にしてしまう $this.css("padding", "0"); var parentWidth = $(this).width(), // 親要素の幅を取得する origFontSize = grabPixelFontSize(); // 全角スペースの幅を仮のフォントサイズとする var newCharPerLine = Math.floor(parentWidth / origFontSize), //- settings.ajustNumber, wordIndex = 0, lineText = "", counter = 0, cnt = 0, postText = "", newFontRatio = fontRatio; fontRatio = (parentWidth / newCharPerLine - origFontSize) / origFontSize; while (wordIndex < words.length) { if( words[wordIndex].length == 0 ){ wordIndex++; if(wordIndex >= words.length) break; } counter = 0; while (counter < words[wordIndex].length ) { idealCharPerLine = newCharPerLine; newFontRatio = fontRatio; postText = ""; cnt = 1; while (1) { if( cnt==idealCharPerLine && grabPixelLineSize(postText + words[wordIndex]<pre>[code] , fontRatio) < parentWidth) { idealCharPerLine++; } // 行頭禁則処理(適当) if( cnt==idealCharPerLine && counter+2 <= words[wordIndex].length && words[wordIndex]<pre>[code].match(/[。、/」\))〉》]\]〕】,.""ヶッっ々ゞ・・ー~]/) ) { idealCharPerLine--; counter--; // 行末の } else if(cnt==idealCharPerLine && words[wordIndex]<pre>[code].match(/[/「〈《¥[〔【""]/) ) { idealCharPerLine--; counter--; // デフォルト } else { if( words[wordIndex]<pre>[code] != undefined) { postText += words[wordIndex]<pre>[code]; cnt++; } }; counter++; if( postText.length >= idealCharPerLine ) break; if( grabPixelLineSize(postText, fontRatio) > parentWidth) break; if( counter >= words[wordIndex].length ) break; }; // while3 newFontRatio = (parentWidth / idealCharPerLine - origFontSize) / origFontSize; lineText += '<span style="letter-spacing: ' + newFontRatio + 'em !important">' + postText + '</span>'; }; // while2 lineText += '<br />'; wordIndex++; }; // while1 $this.html(lineText); }); }; })(jQuery);
使い方はこちら。
ダミーにpタグを使うので、対象となるpタグにはクラス名やidをつけて動作させてください。<p class="sample"> 今IT業界は成熟期に入ろうとしています。我々LIGのミッションは、このユビキタス社会にイノベーションを起こす為のビジョンをコミットし、WinWinの関係でソリューションパートナーとしてクライアントのサービスをマネタイズし、常にユーザーエクスペリエンスをソーシャルな形でローンチし、IT革命、いわゆるWeb2.0時代の幕開けを象徴するかのようなグローバルでありながらもクラウドなサービスをアジャイルな形で開発、提供していくことがもっともプライオリティの高いプロジェクトだと考えています。 <p> <script src="js/jquery-1.4.4.min.js"></script> <script src="js/jquery.test3.js"></script> <script> $(window).load(function() { var userAgent = window.navigator.userAgent.toLowerCase(); if(userAgent.indexOf("webkit") > -1){ $(".sample").adaptText(); } }); </script>
結果はこちら。
全然だめじゃん!
禁則処理はいけてるはずなのに……というわけで、デベロッパーツールを見てみると禁則処理とかは大丈夫。字間もそこそこ調整されてるのに、何故……。
処理重くなるけどそれでもやる
もう、一行の文字数とかカウントしないでやってみるってどうなんでしょう。
- 親要素の幅を文字サイズで割って、1行に入る仮の文字数を出す。
- 親要素の幅を文字数で割って、仮の字間(letter-spacing)を出す。
- 文章を1行ずつに分割。
- 1行の中の文字を1文字ずつ変数に入れいき、
- 現在の文章に次の文字を足した横幅が親要素の横幅以上になる場合、禁則処理を行い、
- 1行の文字数に合うように字間を調整して、
- spanでletter-spacingを設定して出力していく。
/*! jQuery slabtext plugin v2 MIT/GPL2 @freqdec */ (function( $ ){ $.fn.adaptText = function(options) { var settings = { "ajustNumber" : 3 }; return this.each(function(){ if(options) { $.extend(settings, options); }; var $this = $(this), words = $(this).text().split("\n"), origFontSize = $("body").css("font-size").replace(/px/g, ""), idealCharPerLine = null, fontRatio; // 1emの全角スペースの幅を測る function grabPixelFontSize() { var dummy = '<div id="dummy" style="display:inline-block;font-size:' + $this.css("font-size") + ';margin:0;padding:0;"> </div>'; $this.append(dummy); emW = $("#dummy").width(); $("#dummy").remove(); return emW; }; // 現在の文章の幅を測る function grabPixelLineSize(text, ratio) { var dummy = '<p id="dummy" style="-webkit-margin-before:0; -webkit-margin-after:0; margin:0; padding:0;display: inline-block;"><span style="letter-spacing: ' + ratio + 'em !important">' + text + '</span></p>'; $("body").append(dummy); emW = $("#dummy").width(); $("#dummy").remove(); return emW; }; // 親要素の禁則処理を無効にする $this.css("word-break", "break-all"); // 親要素のpaddingも無効にしてしまう $this.css("padding", "0"); var parentWidth = $(this).width(), // 親要素の幅を取得する origFontSize = grabPixelFontSize(); // 全角スペースの幅を仮のフォントサイズとする var newCharPerLine = Math.floor(parentWidth / origFontSize) - settings.ajustNumber, wordIndex = 0, lineText = "", counter = 0, cnt = 0, postText = "", newFontRatio = fontRatio; fontRatio = (parentWidth / newCharPerLine - origFontSize) / origFontSize; while (wordIndex < words.length) { if( words[wordIndex].length == 0 ){ wordIndex++; if(wordIndex >= words.length) break; } counter = 0; newFontRatio = fontRatio; while (counter < words[wordIndex].length ) { postText = ""; cnt = 1; while (1) { // 行頭禁則処理(適当) if( grabPixelLineSize(postText + words[wordIndex]<pre>[code] , fontRatio) >= parentWidth && counter+2 <= words[wordIndex].length && words[wordIndex]<pre>[code].match(/[。、/」\))〉》]\]〕】,.""ヶッっ々ゞ・・ー~]/) ) { break; // 行末禁則処理(適当) } else if(grabPixelLineSize(postText + words[wordIndex]<pre>[code] , fontRatio) >= parentWidth && words[wordIndex]<pre>[code].match(/[/「〈《¥[〔【""]/) ) { break; // デフォルト } else { if( words[wordIndex]<pre>[code] != undefined) { postText += words[wordIndex]<pre>[code]; cnt++; } }; counter++; if( grabPixelLineSize(postText, newFontRatio) >= parentWidth ) break; if( counter >= words[wordIndex].length ) break; }; // while3 newFontRatio = (parentWidth / cnt - origFontSize) / origFontSize; lineText += '<span style="letter-spacing: ' + newFontRatio + 'em !important">' + postText + '</span>'; }; // while2 lineText += '<br />'; wordIndex++; }; // while1 $this.html(lineText); }); }; })(jQuery);
期待に胸膨らませ。
だめでした。
デベロッパツールだとこんな感じです。
なんか字間の計算がおかしいですか?
すみません、数学苦手だったんです……。この問題の周辺事情
この件は、電子書籍業界ではかなり以前から問題になっているらしく、下記のブログでも2010年に触れられています。また、電子出版の方面W3Cのドラフト等々の情報もあります。
- text-align: justify で日本語テキストも両端揃えにしたい! - CSS組版ブログ:
http://blog.antenna.co.jp/CSSPage2/archives/9 - 【目指せePub出版】Webkitでtext-align:justifyに挑戦する - 高橋文樹.com:
http://takahashifumiki.com/web/design/962/
また他にも、下記ブログでは「半角文字を全ての文字の間に入れ込むことで両端揃えを実現」していらっしゃるのですが、今回は「見た目」と「内容(文章として成立する)」の両立が欲しかったので、この方法は採りませんでした。
まとめ
WebKit系で全角・半角が混じった文章を両端揃えするのはとても難しそうです。
「Chrome/Safariでガタガタなんですけど何とかなりませんか」とクライアントさんから言われたら、「こんなこともしたんですけど、ブラウザの仕様上、やっぱりダメみたいなんです……。」と説明してみましょう。
なにかの参考になったら幸いです。
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。