開発misc-dev完了
フローチャート×スクロールアニメーションの実装ガイド
概要
スクロールに応じてフローチャートが段階的にハイライトされ、矢印が描画される、業務フロー説明に最適なインタラクティブUIを実装しました。左側のテキストは紙芝居のように画面中央に固定され、次の要素が来るまで読みやすい位置に留まります。
実装ファイル
apps/web/app/pages/animation-demo.vue
主要な機能
1. 紙芝居スタイルのテキスト表示
各説明セクションが画面中央に固定され、スクロールすると次のセクションが下から現れて前のセクションを押し上げる形で表示されます。
実装のポイント:
.step {
min-height: 150vh;
position: relative;
}
.step-content {
position: sticky;
top: calc(50vh - 100px);
background: #ffffff;
padding: 1rem 0;
z-index: 1;
}
.step[data-step="0"] .step-content { z-index: 1; }
.step[data-step="1"] .step-content { z-index: 2; }
.step[data-step="2"] .step-content { z-index: 3; }
.step[data-step="3"] .step-content { z-index: 4; }
.step[data-step="4"] .step-content { z-index: 5; }
min-height: 150vh: 各セクションに十分なスクロール領域を確保.step-contentにposition: sticky+top: calc(50vh - 100px): コンテンツを画面中央に固定z-indexの昇順設定: 後の要素が前の要素を覆うことで、紙芝居のように表示
2. フローチャートのSVG実装
業務フローを表現する標準的なフローチャート図形を使用:
- 楕円: 開始・終了ノード
- 矩形: 処理ノード
- ひし形: 判断ノード
- 台形: データ保存ノード
- 矢印: フローの方向を示す
<svg class="flowchart" viewBox="0 0 800 900">
<!-- 開始ノード(楕円) -->
<g class="node node-start">
<ellipse cx="400" cy="80" rx="80" ry="40" class="node-shape" />
<text x="400" y="88">開始</text>
</g>
<!-- 処理ノード(矩形) -->
<g class="node node-process">
<rect x="320" y="180" width="160" height="70" rx="8" class="node-shape" />
<text x="400" y="210">データ収集</text>
</g>
<!-- 判断ノード(ひし形) -->
<g class="node node-decision">
<path d="M 400 310 L 500 380 L 400 450 L 300 380 Z" class="node-shape" />
<text x="400" y="375">データ検証</text>
</g>
</svg>
3. スクロール連動アニメーション
進捗値の計算
function updateProgress() {
const vh = window.innerHeight || 0;
steps.forEach((el, i) => {
const r = el.getBoundingClientRect();
const start = vh * 0.2;
const end = vh * 0.8;
const denom = (r.height + start) - end;
let mid = denom !== 0 ? (start - r.top) / denom : 0;
if (mid < 0) mid = 0;
if (mid > 1) mid = 1;
progressValues.value[i] = mid;
if (mid > 0.3 && mid < 0.9) setActive(i);
});
}
- 画面の20%〜80%の範囲で進捗を計算
- 各セクションに0〜1の進捗値を割り当て
- 進捗値0.3〜0.9の範囲でアクティブ状態に
ノードのハイライト
function getNodeStyle(index) {
const isActive = activeIndex.value === index;
const progress = getProgress(index);
const opacity = Math.min(1, progress * 2);
if (isActive) {
return {
opacity: opacity,
transform: `scale(${1 + progress * 0.15})`,
transformOrigin: 'center',
};
}
return {
opacity: activeIndex.value > index ? 1 : opacity * 0.5,
transform: 'scale(1)',
};
}
- アクティブなノードは最大15%拡大
- 非アクティブなノードは透明度を下げる
- 通過済みのノードは完全に表示
矢印の描画アニメーション
function getArrowStyle(stepIndex, isErrorPath = false) {
const progress = getProgress(stepIndex);
const length = isErrorPath ? 350 : 100;
return {
strokeDasharray: length,
strokeDashoffset: length * (1 - progress),
opacity: Math.min(1, progress * 1.5),
};
}
stroke-dasharrayとstroke-dashoffsetで線を徐々に描画- 進捗値に応じて
dashoffsetを減少させる - エラーパスは長さが異なるため個別に設定
4. スタイリング
ノードの状態別スタイル
/* 通常状態 */
.node-shape {
fill: #f3f4f6;
stroke: #9ca3af;
stroke-width: 2;
transition: fill 0.3s ease, stroke 0.3s ease, filter 0.3s ease;
}
/* アクティブ状態 */
.node.active .node-shape {
fill: #dbeafe;
stroke: #2563eb;
stroke-width: 3;
filter: drop-shadow(0 0 16px rgba(37, 99, 235, 0.4));
}
/* ノードタイプ別の色 */
.node-decision.active .node-shape {
fill: #fef3c7;
stroke: #d97706;
filter: drop-shadow(0 0 16px rgba(217, 119, 6, 0.4));
}
.node-data.active .node-shape {
fill: #d1fae5;
stroke: #059669;
filter: drop-shadow(0 0 16px rgba(5, 150, 105, 0.4));
}
矢印のスタイル
.arrow {
fill: none;
stroke: #9ca3af;
stroke-width: 2.5;
transition: stroke 0.3s ease, stroke-width 0.3s ease;
}
.arrow.active {
stroke: #2563eb;
stroke-width: 3;
}
.arrow-error {
stroke: #ef4444;
}
5. レスポンシブ対応
@media (max-width: 900px) {
.wrap {
grid-template-columns: 1fr;
}
.viz {
order: -1;
margin-bottom: 32px;
height: 60vh;
position: relative;
top: 0;
}
.step {
min-height: 80vh;
}
.step-content {
top: calc(40vh - 80px);
}
}
- モバイルでは右側のフローチャートを上部に配置
- グリッドレイアウトを1カラムに変更
- ステップの高さとコンテンツの位置をモバイル向けに調整
技術的なポイント
IntersectionObserver の活用
io = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((e) => e.isIntersecting)
.sort(
(a, b) =>
Math.abs(a.intersectionRect.y + a.intersectionRect.height / 2 - window.innerHeight / 2) -
Math.abs(b.intersectionRect.y + b.intersectionRect.height / 2 - window.innerHeight / 2)
);
if (visible[0]) {
const idx = steps.indexOf(visible[0].target);
if (idx !== -1) setActive(idx);
}
},
{ root: null, threshold: 0.4 }
);
- 画面中央に最も近い要素をアクティブに
threshold: 0.4で要素の40%が表示された時点で検出
SVGマーカーの動的適用
<path
class="arrow"
:marker-end="activeIndex >= 1 ? 'url(#arrowhead-active)' : 'url(#arrowhead)'"
:style="getArrowStyle(1)"
/>
- アクティブ状態に応じて矢印の色を変更
<defs>内で定義したマーカーを動的に参照
パフォーマンス最適化
window.addEventListener('scroll', updateProgress, { passive: true });
passive: trueでスクロールパフォーマンスを向上- 進捗計算をリアクティブな値として保存し、再描画を最適化
使用例
このパターンは以下のような場面で有効です:
- 業務フロー説明: データ処理パイプライン、承認フローなど
- チュートリアル: ステップバイステップの説明
- プロセス可視化: システムアーキテクチャ、ワークフローの説明
- 技術ドキュメント: API呼び出しフロー、エラーハンドリングなど
まとめ
この実装により、以下を実現しました:
- ✅ 読みやすい紙芝居スタイルのテキスト表示
- ✅ スクロールに応じた段階的なフローチャートのハイライト
- ✅ 滑らかな矢印描画アニメーション
- ✅ 白背景で視認性の高いデザイン
- ✅ レスポンシブ対応
poolside.aiの実装を参考にしつつ、業務フロー説明に特化した実用的なコンポーネントとなっています。
参照
- 実装ファイル:
apps/web/app/pages/animation-demo.vue - デモURL:
/animation-demo