商品サンプル画像
ホットウィール(Hot Wheels) ベーシックカー ホンダ シビック カスタム 乗り物おもちゃ ミニカー 3歳から レッド JFN58
商品ページ
Amazon
収益広告(自動登録)
※Amazonレビュー要確認
商品サンプル画像
タカラトミー(TAKARA TOMY) トミカプレミアムunlimited 09 新世紀GPXサイバーフォーミュラ アスラーダG.S.X (風見 ハヤト) ミニカー おもちゃ 6歳以上
商品ページ
Amazon
収益広告(自動登録)
商品サンプル画像
ホットウィール(Hot Wheels) ベーシックカー BMW 635 CSI 乗り物おもちゃ ミニカー 3歳から ブラック JFN61
商品ページ
Amazon
収益広告(自動登録)
商品サンプル画像
アシュリー、魔法はよいこになってから!(全3巻)
商品ページ
Amazon
非収益広告(手動登録)
商品サンプル画像
BANDAI SPIRITS(バンダイ スピリッツ) 30MS アイドルマスター 如月千早 (20th Anniv. YOU AND アイ!) 色分け済みプラモデル
商品ページ
Amazon
収益広告(自動登録)
記事の概要
htmlタグ混在のテキストを1行ずつ左から右へワイプ表示する
作成日:2025-06-16
最終更新日:2025-06-16
記事の文字数:9305
本記事のトピック
  • 概要
  • 全文コード
  • 実装方法:1. 表示領域を制限して全文はスクロールして見られるようにする
  • 実装方法:2. テキストは一行ずつ表示して、表示領域を超える場合は自動的にスクロールするようにする
  • 実装方法:3. テキストを左からワイプ表示する
  • 実装方法:4. テキスト内にhtmlタグが含まれる場合、そのままの形で表示する
  • 以上
htmlタグ混在のテキストを1行ずつ左から右へワイプ表示する
概要

ここでは以下のようにテキストを一行ずつ左から右に表示するJavaScriptコードを載せています。

テキストを1文字ずつ表示する方法自体は結構他でも見つかりましたが、文字単位ではなく全体をスムーズに表示しつつ、htmlタグを維持する方法は見つからなかったのでここに書いています。

元々以下のことがやりたく実装したものです。

やりたかったこと

  1. VRoidナビゲーターのセリフが長すぎても画面を大幅に圧迫しないように、表示領域を制限して全文はスクロールして見られる
  2. 手動スクロールしなくても全文が読めるように、テキストは一行ずつ表示して、表示領域を超える場合は自動的にスクロールする
  3. テキストは一行ずつ表示するが、その際左の文字から徐々に表示する。また、テキストは一文字ずつなどの、文字単位での表示ではなくテキスト全体を一枚の画像のように見なして表示させる(=左からのワイプ
  4. テキスト内にhtmlタグが含まれる場合、当然それはそのままの形で表示する

やる前は簡単に実装できると思ったのですが、「3」と「4」が思ったより難しく、想定よりもかなり時間がかかりました。

全文コード

本記事はどのように実装したかの説明がメインではありますが、以下コードが実際に作成したモノです。

JavaScript / CSS全文コード
※こちらは返礼特典のコンテンツです

こちらのJavaScriptとCSSコードを読み込んで、TextShowAnimation.fadeLeftToRight(showElement, htmlText)メソッドを呼び出せば、一行ずつ表示されるようになると思います。

実装方法:1. 表示領域を制限して全文はスクロールして見られるようにする

VRoidナビゲーターは(非表示にしない限りは)常に画面の下側に出ているものです。

なので、画面領域を大幅に圧迫するのを避ける必要があり、今までは長すぎるセリフを言わせないようにしてそれを制御していたのですが、最近は長すぎるセリフを登録できないのが重荷になってしまいました。

ということで長すぎるセリフを登録しても表示領域を圧迫しないように明示的に表示領域を制限し、全文は手動スクロールすることで見られるようにしました。

実装方法は以下です。

コード(CSS)

display: inline-block; max-height: 180px; overflow-y: scroll; overflow-x: hidden; scrollbar-color: transparent transparent; scrollbar-width: thin; overflow-wrap: break-word; word-break: break-all;

これはCSSの基本的なところなので難しいところは無いかもしれませんが一応……。

max-height: 180px;

要素の最大の高さ。この高さを超える場合はここで指定した値に高さが制限される。これやheightで高さを制限しないとoverflowが意味を為さない。

overflow-y: scroll;

要素の中身(=セリフ)が吹き出しの高さを超えた場合の処理定義。今回はスクロールするようにしている。

scrollbar-color: transparent transparent;
scrollbar-width: thin;

スクロールバーが出ないように(目立たないように)する。

overflow-wrap: break-word;
word-break: break-all;

テキストを自動で折り返すようにする。これは後々の処理のために定義しているところが大きい。

実装方法:2. テキストは一行ずつ表示して、表示領域を超える場合は自動的にスクロールするようにする

上にある通り、画面領域を圧迫しないように表示領域を制限して、全文を読む場合にはユーザが手動でスクロールするようにしています。

しかしこれだとわざわざ手動でスクロールしないといけなくなります。
VRoidナビゲーターは基本的にサイト内ニュースをながら見するための機能なので、このような手動操作は基本的に想定していません

なので原則スクロールは自動でするようにします。

実装方法ですが、後述の処理で「テキストを一行ずつ表示」するようにするので、その処理の中に以下のコードを入れます。

コード(JS)

element.scrollTop = element.scrollHeight;
実装説明

これも難しいところはないかもしれませんが一応……。

elementがテキストを表示する領域です。

element.scrollTopが縦方向のスクロール位置で、例えば0を設定すると一番上にスクロールされます。

element.scrollHeightがスクロール可能な領域全体の高さです。

element.scrollHeight = element.scrollTopとすることで、自動的に最下部にスクロールします。
実際には最下部よりさらに下にスクロールする気がしますが、最下部より大きい値を設定しても最下部ちょうどの値になるように丸められるそうです。

ちなみにoverflow-y: scrollになっていない場合はこの処理は無視されます。エラー回避の必要はありません。

実装方法:3. テキストを左からワイプ表示する
ワイプアニメーションのやり方

前述の自動スクロール処理があるとは言っても、全行のテキストをまとめた出したら意味が無いのでテキストは一行ずつ表示する必要があります。

このとき10秒置きなどで各行をパッと一瞬で表示しても問題はないですが、より自然な表示になるように、各行は左から右へワイプのように表示します。

まずワイプするアニメーションですが、これはCSSのマスクを使っています。

コード(CSS)

.wipeLeftToRight { display: block; mask-image: linear-gradient(to right, black 0%, black 50%, transparent 100%); mask-size: 0% 100%; mask-position: 0 0; mask-repeat: no-repeat; -webkit-mask-image: linear-gradient(to right, black 0%, black 50%, transparent 100%); -webkit-mask-size: 0% 100%; -webkit-mask-position: 0 0; -webkit-mask-repeat: no-repeat; animation-name: wipeLeftToRight; animation-duration: 8s; animation-timing-function: ease; animation-fill-mode: forwards; } @keyframes wipeLeftToRight { to { mask-size: 200% 100%; -webkit-mask-size: 200% 100%; } }

mask-image: linear-gradient(to right, black 0%, black 50%, transparent 100%);でグラデーションした画像をマスクとして、そのマスクを幅0から200%まで徐々に広げることでワイプ表示を実現しています。
200%にしているのはマスク画像の右半分がグラデーションで透過されてしまっているので、100%にしてしまうと、テキストも同様にグラデーションされたままワイプアニメーションが完了してしまうためです。

animation-duration: 8s;として、8秒かけてテキストが表示されるようにしていますが、これでは画面幅に応じて表示速度が変わってしまいます(画面幅が狭いと露骨に表示速度が遅くなります)。

なので、animation-durationはJavaScript内で画面幅に応じて値を変える必要があります(後述)。

テキストを一行ずつのdivに分ける方法

前項のCSSアニメーションとマスク画像を使用して表示する方法ですが、この方法だと要素全体が左から右に表示されます。
すなわちテキストが一行ずつではなく、すべての行が同時に左から右にワイプ表示されてしまいます

なので各行をdivで分けて、一つ一つの行に対してワイプアニメーションを適用するようにしています。

「各行をdivで分けて、一つ一つの行に対してワイプアニメーションを適用する」ために以下のプロセスで実装しています。

1. 各行の文字数を割り出す
2. 割り出した文字数から、実際に各行ごとにテキストを表示する(前項のCSSアニメーションを適用するため各行ごとにdivで囲う)

以下は「1. 各行の文字数を割り出す」コードです(「2」の処理は、次項と大きく関わりがあるため次項に回します)。

コード(JS)

/** * 一行あたりの文字数を返す * 速度計測も行う * * @param {*} argElement 表示先のhtml要素 * @param {*} argText 表示するテキスト * @returns 一行あたりの文字数の配列 */ function getElementInfo(argElement, argText){ const ret = {}; ret.cntCharLengthEachLine = []; ret.width = 0; // 元の要素をいじらないようにcloneする const element = argElement.cloneNode(false); // 引数のテキストにhtmlタグがあれば除去する const doc = new DOMParser().parseFromString(argText, 'text/html'); const text = doc.body.textContent; // テキストを span で 1文字ずつラップ const chars = text.split(''); element.innerHTML = chars.map(char => `${char}`).join(''); const charSpans = [...element.querySelectorAll('.char')]; // 行位置を取得できるように一時的に可視にして、親要素に追加 element.style.display = 'inline-block'; element.style.visibility = 'hidden'; const parentElm = argElement.parentNode; parentElm.appendChild(element); ret.width = element.getBoundingClientRect().width; // 半角文字であっても無条件改行するようにする(CSSの読み込みが完了していない場合は効かない) parentElm.classList.add('tmp-fade-leftToRight'); // 1文字ずつ表示位置を確認して、各行の文字数を保持する let charLengthInLine = 0; let lastTop = null; charSpans.forEach(span => { const rect = span.getBoundingClientRect(); // 次の行に行っているかどうかを見る if (lastTop === null || Math.abs(rect.top - lastTop) < 1) { charLengthInLine++; } else { // 次の行に行ったら、その時点の文字数を保持し、リセットする ret.cntCharLengthEachLine.push(charLengthInLine); charLengthInLine = 1; } lastTop = rect.top; }); if (charLengthInLine){ ret.cntCharLengthEachLine.push(charLengthInLine); } // 作業用の要素を消すのを忘れない parentElm.removeChild(element); parentElm.classList.remove('tmp-fade-leftToRight'); return ret; }

コード(CSS)

.tmp-fade-leftToRight{ overflow-wrap: break-word; word-break: break-all; white-space: normal !important; }

getElementInfoメソッドで各行の文字数を計算しています。

計算とは言いますが、ダミーの要素に実際にテキストを表示して、そこから各行の文字数を割り出しています

getBoundingClientRectメソッドを使えば各文字の表示座標を取得できるので、その表示座標から何行目に表示されているか分かります

こうして各行の文字数が分かれば、あとは元のテキストからその文字数分を各行に表示すればよいだけなので簡単です。

「ret.width = element.getBoundingClientRect().width;」は、画面幅を取っています。これは、前項のアニメーション速度の計算に必要なので取得しています。

実装方法:4. テキスト内にhtmlタグが含まれる場合、そのままの形で表示する

まぁこれはそのままですね。

VRoidナビゲーターには特にaタグがよく含まれており、これを削除してただのプレーンテキストにするのは絶対NGです。

なのでhtmlタグを維持したまま画面上に表示する必要があります。

まず最初に各行の表示用html文を組み立て、その後実際にそのhtml文を表示する、2段階の構成に分けています

まず各行の表示用html文を組み立てる

前項のCSSアニメーションをするために、各行をdivで囲う必要があるのですが、そうすると例えばaタグが1行目から2行目にわたって表示されている場合、一度1行目でaタグを閉じて2行目で再びaタグを設置しないといけません。

例:aタグが複数行に跨る場合、aタグを分割しないといけない

元のテキスト:こんにちは。<a href="/">こちらはテストリンク</a>です。  ↓ 1行目:<div class="fade-leftToRight">こんにちは。<a href="/">こちらは</a></div> 2行目:<div class="fade-leftToRight"><a href="/">テストリンク</a>です。</div>

これを実現しているのが以下のコードです。

コード(JS)

/** * テキストを行ごとのhtml文にする * * @param {*} htmlText 表示するhtmlテキスト * @param {*} charLengthEachLine 1行当たりの文字数(getElementInfoメソッドで取得) * @returns 行ごとに分割したhtml文 */ function splitLines(htmlText, charLengthEachLine) { const ret = new Array(charLengthEachLine.length).fill(''); const htmlTextElm = new DOMParser().parseFromString(htmlText, 'text/html').body; let lineIndex = 0; let charCount = 0; const tagStack = []; function appendChar(char) { if(lineIndex > charLengthEachLine.length){ return; } // 必要ならタグ開始を追加 if (ret[lineIndex] === '' && tagStack.length > 0) { for (const tag of tagStack) { ret[lineIndex] += tag.tagOpener; } } ret[lineIndex] += char; charCount++; // 次の行へ進む if (charCount === charLengthEachLine[lineIndex]) { // 必要ならタグ閉じを追加 if (tagStack.length > 0) { for (let i = tagStack.length - 1; i >= 0; i--) { const tag = tagStack[i]; ret[lineIndex] += tag.tagCloser; } } lineIndex++; charCount = 0; } } function appendNode(node) { for (const child of node.childNodes) { if (child.nodeType === Node.TEXT_NODE) { const chars = child.textContent.split(''); for (const char of chars) { appendChar(char); } } else if (child.nodeType === Node.ELEMENT_NODE) { // タグをhtml文字列で作成 const tagName = child.tagName.toLowerCase(); const tagAttrs = [...child.attributes].map(attr => `${attr.name}="${attr.value}"`).join(' '); const tagOpener = `<${tagName}${tagAttrs ? ' ' + tagAttrs : ''}>`; const tagCloser = ``; // 配列の一番最後にタグを追加 const tag = {}; tag.tagOpener = tagOpener; tag.tagCloser = tagCloser; tagStack.push(tag); // 最初の文字にはappendCharで勝手に開きタグをつけるためスキップする if (ret[lineIndex] !== '') { ret[lineIndex] += tag.tagOpener; } // 再帰的に呼び出し(タグの中にまた別のタグ要素がある可能性もあるため) appendNode(child); // 最後の文字にはappendCharで勝手に閉じタグをつけるためスキップする if (charCount !== charLengthEachLine[lineIndex] && charCount !== 0) { ret[lineIndex] += tag.tagCloser; } // 処理が終わったら一番最後のタグを削除すればよいハズ tagStack.pop(); } } } appendNode(htmlTextElm); return ret; }

appendNodeメソッドでhtml文をNode単位に分けて、タグを追加しつつ各行にappendCharメソッドで文字を追加していっています。

appendNodeメソッドでhtml文を各Nodeに分けて、単なるテキストノードはappendCharメソッドで文字を追加し、aタグなどで囲われているノードはそのタグを追加した後にappendCharメソッドで文字を追加するようにしています。

ざっくりとして説明は以上で、コード内にコメントを記載しているので細かい内容はそこをご確認ください。

実際にhtml文を表示する

前項のsplitLinesメソッドで各行のhtml文は組み立てられたのであとはそれを表示するだけです。

以下はanimateLinesメソッドを再帰的に呼び出して1行ずつテキストを表示しています。

コード(JS)

/** * 実際に画面上にテキストを表示する */ function animateLines(element, lines, animationDuration, lineIndex = 0) { // 最終行まで表示したら終了 if (lineIndex >= lines.length){ return; } // 初回実行時にelementの文字列をリセットする if(lineIndex === 0){ element.innerHTML = ''; } // CSSアニメーションを適用してテキストを表示 const lineDiv = document.createElement('div'); lineDiv.classList.add('fade-leftToRight'); lineDiv.style.animationDuration = `${animationDuration}s`; lineDiv.innerHTML = lines[lineIndex]; element.appendChild(lineDiv); // 自動的に下までスクロールする element.scrollTop = element.scrollHeight; // マスク画像を2倍幅にしているので、次の待ち時間は1/2にする必要がある setTimeout(() => { animateLines(element, lines, animationDuration, lineIndex + 1); }, animationDuration * 1000 / 2); }
以上
以上です。何かの参考にしてください。
コメントログ
※コメントは最新50件が表示されます
コメント投稿




画面下部の「コンタクト」からも連絡可能です。
管理人ツイート
商品サンプル画像
タミヤ 1/24 スポーツカーシリーズ No.373 Honda プレリュード (BF1) プラモデル 24373 (自動車)
商品ページ
Amazon
収益広告(自動登録)
※Amazonレビュー要確認
商品サンプル画像
タカラトミー(TAKARA TOMY) トミカ No.48 ヤマト運輸 EV集配トラック ミニカー おもちゃ 3歳以上
商品ページ
Amazon
収益広告(自動登録)
商品サンプル画像
BANDAI SPIRITS(バンダイ スピリッツ) HG 機動戦士ガンダムZZ ガルスJ 1/144スケール 色分け済みプラモデル
商品ページ
Amazon
収益広告(自動登録)
商品サンプル画像
【通常盤 初回仕様】【先着限定グッズ(ホログラムステッカー(2枚セット))有り】米津玄師 IRIS OUT / (未定) 【チケット2次先行シリアルナンバー(抽選)封入】
商品ページ
Amazon
収益広告(自動登録)
※Amazonレビュー要確認
管理人作品宣伝
【アークナイツ】グムがずっと殴ってくれる動画
動画 / 最終更新:2025-02-14
喝を入れたいあなたに。…喝を入れたいあなたに。

YouTubeで閲覧する利用素材等の詳細情報
【アークナイツ】アークナイツ運動会-関所破壊レース
動画 / 最終更新:2025-01-16
アークナイツ生息演算の岸壁の関の関門を誰が最速で破壊できるかを競います。…アークナイツ生息演算の岸壁の関の関門を誰が最速で破壊できるかを競います。

YouTubeで閲覧するニコニコ動画で閲覧する利用素材等の詳細情報
作品一覧はこちら
関連ページ
本サイトのタグ一覧
NovelAIR18VRoidWebサイト作成Webツールととモノ。アークナイツアークナイツ-ステージ攻略日記アズールレーンアズールレーン-日記ウマ娘ギャラリーゲームデビラビローグネットスラング系プログラミングホラーポケットタウン怪談気ままな日記情報技術情報技術-WebAPI知的財産権統合戦略白夜極光本サイトについて魔王スライム様がんばる!漫画
人気記事
メイド・オブ・ザ・デッド-攻略お助け情報
最終更新日:2024-05-01
スコア:759.2404 pt
ネタバレ注意! 本ページは『メイド・オブ・ザ・デッド』の情報を記録しているものです。 攻略の参考に…
記事を閲覧する
ポケットタウン_パズル一覧
最終更新日:2025-05-02
スコア:679.3478 pt
グレーのピースの数 (Number of gray pieces):検索グレーピースの数を入力して、…
記事を閲覧する
剣と魔法と学園モノ。2G - パーティ編成確認ツール
最終更新日:2024-05-09
スコア:596.2517 pt
ツール概要 ととモノ。2Gのパーティ編成を考える際に使うツールです。 あくまでストーリークリアまで…
記事を閲覧する
地獄先生ぬ~べ~で好きな切ないエピソード
最終更新日:2025-07-08
スコア:308.6642 pt
概要 初代というべきか、週刊少年ジャンプで連載されていた地獄先生ぬ~べ~の切ないエピソードの中で好…
記事を閲覧する
剣と魔法と学園モノ。3 - パーティ編成確認ツール
最終更新日:2025-05-07
スコア:188.1800 pt
ツール概要 ととモノ。3のパーティ編成を考える際に使うツールです。 攻略本や攻略wikiを参考にし…
記事を閲覧する
アークナイツ-常設商品-理性換算
最終更新日:2024-04-28
スコア:112.5000 pt
概要 "常設商品でお得な商品はどれか"というのを理性に換算して一覧化したものとなります。 絶対的に…
記事を閲覧する
本サイトについて
最終更新日:2025-07-22
スコア:103.0099 pt
本サイトの概要 概要 個人ブログのようなものです。とくにジャンルはありません。 本サイト内の情報に…
記事を閲覧する
アークナイツ:生息演算「熱砂秘聞」の攻略メモ
最終更新日:2025-01-13
スコア:99.6923 pt
注意 本ページには攻略情報も一部含まれてるので、そういうのを見たくない人は見ない方が良いです。含ま…
記事を閲覧する
最新記事
スプシを使って柔らかくAPIからログ記録
最終更新日:2025-07-27
概要 Googleスプレッドシート(=スプシ)を使って、クライアント側の情報などを以下のように簡単…
記事を閲覧する
本サイトについて
最終更新日:2025-07-22
本サイトの概要 概要 個人ブログのようなものです。とくにジャンルはありません。 本サイト内の情報に…
記事を閲覧する
地獄先生ぬ~べ~新アニメで設定変わったところ(3話まで)
最終更新日:2025-07-13
概要 地獄先生ぬ~べ~の新アニメが2025-07-02(木)よりやっていますが、そこで設定が変わっ…
記事を閲覧する
地獄先生ぬ~べ~で好きな切ないエピソード
最終更新日:2025-07-08
概要 初代というべきか、週刊少年ジャンプで連載されていた地獄先生ぬ~べ~の切ないエピソードの中で好…
記事を閲覧する
商品サンプル画像
TAMASHII NATIONS S.H.フィギュアーツ(真骨彫製法) 仮面ライダー龍騎 約145mm PVC&ABS製 塗装済み可動フィギュア
商品ページ
Amazon
収益広告(自動登録)
※Amazonレビュー要確認