縦書きアプリでの日本語の文字表示特有の
課題
(「ことのはっと」開発記)

関連記事

1. はじめに

前回の記事では、美しい縦書きの名言画像を作成できるWebアプリ「ことのはっと」の基本機能を実装しました。アプリは正常に動作し、縦書きの文字も表示されていました。しかし、実際に使ってみると、日本語の縦書きならではの細かな問題が次々と見つかります。

今回は、これらの問題を一つずつ解決していく過程で学んだ、日本語縦書き表示の奥深さと技術的な解決策を共有します。

2. 句読点の位置を左下から右上に

次に気づいたのは、「横書きを縦に並べても縦書きにはならない」ということです。
小さな文字は、前の文字との関係で、位置が変わります。

まずは、句点(。)と読点(、)。縦書きでは文字の右上に小さく表示する必要があります。

drawPunctuation(char, x, y, fontSize) {
    this.ctx.font = `${this.fontWeight} ${fontSize * 0.7}px ${this.fontFamily}`;
    
    if (char === '。') {         
        this.ctx.fillText(char, x + fontSize * 0.6, y - fontSize * 0.5);
    } else if (char === '、') {  
        this.ctx.fillText(char, x + fontSize * 0.6, y - fontSize * 0.5);
    }
}
Code language: JavaScript (javascript)

2.1. 「っ」「ゃ」「ゅ」「ょ」の位置調整

「っ」「ゃ」「ゅ」「ょ」などの小さな文字も、縦書きでは右上に配置するのが正しい表示です。これらの文字を判定して、適切な位置に描画する機能を追加しました:

drawSmallKana(char, x, y, fontSize) {
    // 小さい文字は右上に配置
    this.ctx.fillText(char, x + fontSize * 0.3, y - fontSize * 0.3);
}
Code language: JavaScript (javascript)

3. 記号の向きを回転する

次に気付いたのは、一部の記号は縦書きと横書きで向きが違うことです。

3.1. リーダー記号と波線の回転

たとえば、三点リーダー(…)や波線(~)は、縦書きでは90度回転させて表示します。これは、文字の流れに沿って横向きにするためです:

数学的には、90度の回転は Math.PI / 2 ラジアン1で表現されます。

drawLeader(char, x, y, fontSize) {
    // リーダー記号は90度回転
    this.ctx.translate(x, y);
    this.ctx.rotate(Math.PI / 2);
    this.ctx.fillText(char, 0, 0);
}
Code language: JavaScript (javascript)

3.2. Canvas状態管理の重要性

Canvas描画では、座標変換(移動、回転、拡大縮小)の状態管理が重要です。save()restore() を適切に使うことで、他の文字の描画に影響を与えずに変換を適用できます:

drawLeader(char, x, y, fontSize) {
    this.ctx.save();        // 現在の状態を保存
    this.ctx.translate(x, y);
    this.ctx.rotate(Math.PI / 2);
    this.ctx.fillText(char, 0, 0);
    this.ctx.restore();     // 状態を復元
}
Code language: JavaScript (javascript)

3.3. 横書き括弧から縦書き専用記号への変換

ほかの多くの文字も同様に90°回転する方法もあります。
しかし、最も重要な発見は、横書きで使う括弧と縦書きで使う括弧には実は別の字形があるということでした。

例えば、横書きの「」は縦書きでは「﹁﹂」という専用の文字があります。微妙に横棒の長さが異なるのです。

変換マップを作成して、自動的に適切な文字に置き換える仕組みを実装しました:

convertToVerticalBracket(char) {
    const bracketMap = {
        '「': '﹁',  // 鉤括弧(開き)
        '」': '﹂',  // 鉤括弧(閉じ)
        '『': '﹃',  // 二重鉤括弧(開き)
        '』': '﹄',  // 二重鉤括弧(閉じ)
        '(': '︵',  // 丸括弧(開き)
        ')': '︶',  // 丸括弧(閉じ)
        // ...
    };
    
    return bracketMap[char] || char;
}
Code language: JavaScript (javascript)

この変換により、括弧が自然な縦書きの向きで表示されるようになりました。

3.4. Noto Serif JPの採用

ただし、このような字形を使うには、表示するフォントに含まれている必要があります。

そこで、Googleが開発したNoto Serif CJKのJPフォントを優先するように設定しました。

this.fontFamily = "'Noto Serif JP', 'Times New Roman', 'Yu Mincho', ...";
Code language: JavaScript (javascript)

このフォントは縦書きに最適化されており、文字カバレッジ2(対応文字数)も非常に広いという特長があります。

複数のフォントを指定することで、Noto Serif JPが利用できない環境でも適切なフォントが選択されます。

オープンソースフォントなので、自分のサイトでもホスティングして読み込むようにしました。

4. さらなる文字種への対応

基本的な文字の表示が改善されると、さらに多くの文字種に対応したくなりました。

文字の表示について調べていると、Adobe Illustratorで縦書きテストを行うJavaScriptコードを発見しました。これは実際のデザイン現場で使われているもので、縦書きで問題になりやすい文字が体系的に整理されていました。

texts = [
  '■あ亜',        // 基本文字
  '‥…、。',      // 句読点・リーダー
  '〈〉《》「」',   // 各種括弧
  '『』【】〔〕',   // 各種括弧
  'ぁぃぅぇぉ',    // ひらがな小文字
  'っゃゅょ',      // ひらがな小文字
  // ...
];
Code language: JavaScript (javascript)

このリストを参考に、「ことのはっと」でも対応が必要な文字を特定しました。

4.1. Canvas描画エンジンでの文字種別判定の体系化

多くの文字種に対応するため、描画エンジンの構造を整理しました。

各文字を適切なカテゴリに分類する仕組みを作りました:

drawCharacter(char, x, y, fontSize) {
    this.ctx.save();
    
    if (this.isPunctuation(char)) {
        this.drawPunctuation(char, x, y, fontSize);
    } else if (this.isBracket(char)) {
        this.drawBracket(char, x, y, fontSize);
    } else if (this.isQuote(char)) {
        this.drawQuote(char, x, y, fontSize);
    } else if (this.isLeader(char)) {
        this.drawLeader(char, x, y, fontSize);
    } else if (this.isSmallKana(char)) {
        this.drawSmallKana(char, x, y, fontSize);
    } else {
        this.ctx.fillText(char, x, y);
    }
    
    this.ctx.restore();
}
Code language: JavaScript (javascript)

この構造により、新しい文字種への対応が容易になりました。各判定メソッドは単純な文字列検索で実装されています:

isSmallKana(char) {
    const smallKana = 'っゃゅょァィゥェォヮヵヶぁぃぅぇぉッャュョゎゕゖ';
    return smallKana.includes(char);
}
Code language: JavaScript (javascript)

4.2. 伸ばし棒の変換

伸ばし棒「ー」は、ただ文字種類を変更したり、90°回転するだけではうまくいきませんでした。
明朝体の場合には、起筆があるからです。

そこで、90°回転から少しずらして、左右反転にしました。

    drawLongVowel(char, x, y, fontSize) {
        if ('ー|−―'.includes(char)) {
            const verticalChar = this.convertToLongVowel(char);
            this.ctx.translate(x, y);
            this.ctx.rotate(Math.PI / 2 - 0.07); // 90度回転
            this.ctx.scale(1, -1);
            this.ctx.fillText(verticalChar, 0, 0);
        }
    }Code language: JavaScript (javascript)

4.3. 引用符の縦書き変換(構文解析)

英語の引用符「””」は、日本語の縦書きでは「〝〟」という専用の文字を使います:

convertToVerticalQuote(char) {
    const quoteMap = {
        '"': '〝',  // 開き引用符
        '"': '〟',  // 閉じ引用符
        "'": '‛',   // 開きシングル引用符
        "'": ''',   // 閉じシングル引用符
    };
    
    return quoteMap[char] || char;
}
Code language: PHP (php)

ただし、これは同じ記号が2つの文字になるので、引用範囲の対応関係を確認しないといけません。

5. 見栄えの微調整

作ってみると、いくつか見栄えにも気になる点が出てきました。

5.1. アイコンの変更

「ことのはっと」のアイコンをシルクハットにしてみました。

グリム童話の挿絵みたいなペン画タッチのちょっとクラシックスタンダードな感じで、シルクハットとペンをアイコンにしてみました。

6. 中央揃えから上揃えへの変更

最初は文字を中央に配置していましたが、これを上揃えに変更する必要がありました。

Canvas3という、ブラウザで図形や文字を描くための技術を使って文字を表示していました。

// 変更前:中央揃え
const startY = this.padding.top + (availableHeight - totalTextHeight) / 2;

// 変更後:上揃え
const startY = this.padding.top + fontSize / 2 + indentOffset;
Code language: JavaScript (javascript)

この変更により、文字が上から順番に並ぶようになりました。ただし、新たに indentOffset(字下げ量)という値を追加しています。これには理由があります。

6.1. 行頭括弧による字下げ機能

括弧で始まる行がある場合、他の行を少し下げて読みやすくすることにしました。

例えば、次のような文章があるとします:

美しい朝だった。
「おはよう」
彼女は言った。

この場合、「美しい朝だった。」と「彼女は言った。」を少し下げると、文字の部分がそろって、読みやすくなります。

実装では、まずテキスト全体をスキャンして括弧で始まる行があるかを調べます:

hasAnyLeadingBrackets(lines) {
    return lines.some(line => this.hasLeadingBracket(line));
}
Code language: JavaScript (javascript)

括弧で始まる行が見つかった場合、括弧のない行に字下げを適用します:

const indentOffset = hasBracketLines && !currentLineHasBracket ? 
    fontSize * this.charSpacing : 0;
Code language: JavaScript (javascript)

この仕組みにより、自然な日本語縦書きのレイアウトが実現できました。

6.2. フォントウェイトの調整

文字をより力強く見せるため、フォントの太さ(ウェイト)4を通常の400から500に変更しました。

this.fontWeight = "500";  // 通常400より太く、太字700より細く
this.ctx.font = `${this.fontWeight} ${fontSize}px ${this.fontFamily}`;
Code language: JavaScript (javascript)

この微調整により、文字の存在感が増し、読みやすさが向上しました。

6.3. 文字配置アルゴリズムの最適化

文字の間隔も重要な要素です。読みやすさを追求して、字間と行間を調整しました:

this.lineSpacing = 2.5; // 横方向の列間隔
this.charSpacing = 1.1; // 縦方向の文字間隔
Code language: JavaScript (javascript)

これらの数値は、実際にさまざまなテキストで表示確認を行い、最も読みやすい値を見つけました。

7. ユーザーインターフェースの統一

機能面の改善と並行して、ユーザーインターフェースの一貫性も向上させました。

アプリのヘッダーを3カラムレイアウトに変更しました。左にアイコン、中央にタイトル、右に共有ボタンという配置です:

.app-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    position: relative;
}

.app-title {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
}
Code language: CSS (css)

CSSの position: absolutetransform を組み合わせることで、タイトルを確実に中央に配置できます。

また、紙の色選択とインクの色選択で異なっていたデザインを統一しました。どちらも小さなプレビュー画像とテキストラベルの組み合わせに変更し、操作の一貫性を高めました。

7.1. PWA機能のキャッシュ戦略の見直し

アプリの実用性を高めるため、PWA5(Progressive Web App)機能も改善しました。

開発中にキャッシュが残ってしまい、最新の変更が反映されない問題が発生しました。Service Worker6(ブラウザがバックグラウンドで実行するプログラム)でバージョン管理を行う仕組みを導入しました。

const APP_VERSION = '1.65';
const CACHE_NAME = `kotonohat-v${APP_VERSION}`;

self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheName !== CACHE_NAME) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});
Code language: JavaScript (javascript)

バージョン番号を変更すると、古いキャッシュが自動的に削除され、常に最新版が表示されます。

8. ブラウザ間の差異と文字体系の奥深さ

この開発過程で、日本語のWeb表示には想像以上に多くの考慮点があることを学びました。

日本語は、ひらがな、カタカナ、漢字、英数字、記号など多様な文字体系が混在する複雑な言語です。それぞれに縦書きでの適切な表示方法があり、すべてに対応するには継続的な改善が必要です。

同じコードでも、Chrome、Firefox、Safariで微妙に表示が異なることがあります。特に縦書き機能は比較的新しい技術7のため、ブラウザごとの実装の違いが現れやすい分野です。

文字種別の判定を毎回行うため、大量の文字を表示する場合のパフォーマンスも重要です。現在の実装では文字列検索を使っていますが、将来的にはより効率的な方法を検討する必要があります。

8.1. まとめ

「ことのはっと」の文字表示改善を通じて、日本語縦書きWebアプリケーション開発の技術的課題と解決策を詳しく検証しました。文字配置アルゴリズムの最適化、特殊文字への体系的対応、Canvas描画エンジンの構造化、そしてPWA機能の改善により、実用的なアプリケーションが実現できました。

特に重要だったのは、Adobe Illustratorなどの既存ツールの知見を活用することで、Web技術でも高品質な日本語縦書き表示が可能になることです。ブラウザ間の差異やパフォーマンス考慮も含めて、継続的な改善が日本語Webアプリケーションの品質向上につながります。


  1. CSS Writing Modes Level 3 – W3C勧告 – 縦書きレイアウトを実現するCSS仕様の公式ドキュメント
  2. Canvas API – Web APIs | MDN – ブラウザでの図形・文字描画技術の詳細なリファレンス
  3. 日本語組版処理の要件 – W3C勧告 – 日本語の縦書きレイアウトに関する国際標準仕様
  4. 縦書きHTMLにおける文字の向きはどのように定まるか – ドワンゴ教育サービス開発者ブログ – 縦書き文字表示の技術的課題と解決策の詳細な解説
  5. Google Fonts + 日本語 – Noto Serif JPなど日本語Webフォントの公式情報
  6. Service Worker API – Web APIs | MDN – PWAのキャッシュ制御を実現するService Worker技術の解説
  7. プログレッシブ ウェブアプリ | web.dev – PWA開発のベストプラクティスを解説するGoogle公式ガイド
  8. Unicode Vertical Text Layout – Unicode Standard Annex #50 – 縦書き文字の向きを定める国際標準仕様
  9. HTMLの文章を縦書にする方法を解説! – DMM WEBCAMP MEDIA – Web縦書き実装の基本的な方法とブラウザ対応状況
  10. font-weight – CSS | MDN – フォントの太さ調整に関するCSS仕様の詳細
  1. ラジアンは角度の単位で、1ラジアン≒57.3度。円周率π(パイ)ラジアンが180度に相当するため、π/2ラジアンが90度となる – ラジアン – Wikipedia
  2. 文字カバレッジとは、フォントが対応している文字の範囲や種類のこと。Noto Serif JPは日本語の漢字、ひらがな、カタカナに加え、多言語文字もサポートしている – Google Fonts + 日本語
  3. Canvas APIは、HTML5で導入されたブラウザ上で図形や文字を描画するためのJavaScript API。2DグラフィックスやWebGLを使った3Dグラフィックスの描画が可能 – Canvas API – Web APIs | MDN
  4. フォントウェイトは文字の太さを表す数値で、100(極細)から900(極太)まで9段階。400が標準、700が太字として一般的に使用される – font-weight – CSS | MDN
  5. Progressive Web App(PWA)は、Webサイトをネイティブアプリのような体験で利用できる技術。オフライン動作、プッシュ通知、ホーム画面への追加などが可能 – プログレッシブ ウェブアプリ | web.dev
  6. Service Workerは、Webページとは独立してブラウザがバックグラウンドで実行するJavaScriptプログラム。キャッシュ制御、プッシュ通知、バックグラウンド同期などの機能を提供 – Service Worker API – Web APIs | MDN
  7. CSS Writing Modes Level 3は2019年に勧告されたCSS仕様。vertical-rl、text-orientationなどのプロパティで縦書きレイアウトを実現できる – CSS Writing Modes Level 3