モダンUIコンポーネントの実装パターン
実装ガイドReact、Vue、Angularで共通して使えるUIパターンを解説
スクロール連動で要素に視差効果を適用する高性能パララックスコンポーネント。リアルタイムデータ連携、動的要素生成、カスタムアニメーション制御に対応。Intersection Observer APIとRequestAnimationFrameで最適化されたアニメーション
スクロールすると、各レイヤーが異なる速度で動きます
スクロールに応じて透明度と位置が変化します
画面に入ると徐々に表示されます
スムーズなトランジション効果
スクロールで表示されます
スクロールで表示されます
スクロールで表示されます
スクロールで回転
スクロールで上下
スクロールで拡大縮小
訪問者数カウンター
進捗: 0%
売上推移チャート
Intersection Observer APIとRequestAnimationFrameによる最適化で、滑らかで軽量なパララックス効果を実現
リアルタイムデータ連携、動的要素生成、関数ベースの値計算により、データドリブンなパララックス体験を実現
移動、回転、スケール、透明度、ぼかし効果など多彩な変換を組み合わせた複雑なアニメーション。キーフレームアニメーション対応
パララックス背景、動的カウンター、リアルタイムチャート、プログレスバーなど即座に使える動的プリセット
プロンプト例:
リアルタイムデータに基づいて変化する動的パララックスダッシュボードを作成してください。API連携によるデータ取得、動的カウンター、プログレスバー、チャートのアニメーション、データ変化に応じた色彩変化、アラート表示システムを実装してください。
プロンプト例:
スクロールで進行するインタラクティブなストーリー体験を作成してください。動的コンテンツ生成によるキャラクター変化、章ごとのパララックス背景変化、リアルタイム選択肢分岐、ユーザー行動に応じたストーリー展開、進捗同期型音響効果を含めてください。
プロンプト例:
ゲーム要素を取り入れた動的パララックス体験を開発してください。ユーザーアクションによるスコア計算、リアルタイムランキング、実績システム、スクロール連動レベルアップ、動的報酬表示、ソーシャル機能連携を実装してください。
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
/**
* Dynamic Scroll-triggered Parallax - 動的スクロール連動パララックスエフェクト
*
* 高性能なスクロール連動アニメーションを提供し、視差効果や要素の動的変化を実現
* リアルタイムデータ連携、動的要素生成、カスタムアニメーション制御に対応
* Intersection Observer APIとRequestAnimationFrameで最適化されたパフォーマンス
*/
// 動的データの型定義
export interface DynamicData {
[key: string]: any;
}
// アニメーションキーフレーム
export interface AnimationKeyframe {
progress: number;
values: {
translateX?: number;
translateY?: number;
rotate?: number;
scale?: number;
opacity?: number;
blur?: number;
[key: string]: any;
};
}
// 動的アニメーション設定
export interface DynamicAnimation {
keyframes: AnimationKeyframe[];
duration?: number;
repeat?: boolean;
direction?: 'normal' | 'reverse' | 'alternate';
}
export interface ParallaxElementProps {
/** 要素の一意識別子 */
id?: string;
/** 表示するコンテンツ(関数で動的生成可能) */
children: React.ReactNode | ((data: DynamicData, progress: number) => React.ReactNode);
/** パララックス速度(-1.0 〜 1.0、0は固定) */
speed?: number | ((data: DynamicData) => number);
/** Y軸移動量(px) */
translateY?: number | ((progress: number, data: DynamicData) => number);
/** X軸移動量(px) */
translateX?: number | ((progress: number, data: DynamicData) => number);
/** 回転量(度) */
rotate?: number | ((progress: number, data: DynamicData) => number);
/** スケール変化(0.1 〜 3.0) */
scale?: number | ((progress: number, data: DynamicData) => number);
/** 透明度変化(0.0 〜 1.0) */
opacity?: number | ((progress: number, data: DynamicData) => number);
/** ぼかし効果(px) */
blur?: number | ((progress: number, data: DynamicData) => number);
/** 開始位置(0.0 〜 1.0、画面に対する位置) */
startOffset?: number;
/** 終了位置(0.0 〜 1.0、画面に対する位置) */
endOffset?: number;
/** イージング関数 */
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'elastic';
/** 動的アニメーション設定 */
dynamicAnimation?: DynamicAnimation;
/** 動的データ */
dynamicData?: DynamicData;
/** データ更新間隔(ms) */
updateInterval?: number;
/** 3D変換を有効化 */
enable3D?: boolean;
/** GPU加速を強制 */
forceGPU?: boolean;
/** カスタムクラス */
className?: string;
/** 無効化 */
disabled?: boolean;
/** スクロール進捗コールバック */
onProgress?: (progress: number, element: HTMLElement, data: DynamicData) => void;
/** 要素がビューポートに入った時のコールバック */
onEnter?: (element: HTMLElement, data: DynamicData) => void;
/** 要素がビューポートから出た時のコールバック */
onLeave?: (element: HTMLElement, data: DynamicData) => void;
/** データ変更時のコールバック */
onDataChange?: (newData: DynamicData, oldData: DynamicData) => void;
}
export interface ParallaxContainerProps {
/** コンテナの内容 */
children: React.ReactNode;
/** スクロール感度 */
sensitivity?: number;
/** パフォーマンスモード */
performance?: 'high' | 'balanced' | 'smooth';
/** デバッグモード */
debug?: boolean;
/** グローバル動的データ */
globalData?: DynamicData;
/** データプロバイダー関数 */
dataProvider?: () => Promise<DynamicData> | DynamicData;
/** データ更新間隔(ms) */
dataUpdateInterval?: number;
/** リアルタイムモード */
realtime?: boolean;
/** カスタムクラス */
className?: string;
/** 無効化 */
disabled?: boolean;
/** グローバルデータ変更時のコールバック */
onGlobalDataChange?: (newData: DynamicData, oldData: DynamicData) => void;
}
// イージング関数
const easingFunctions = {
linear: (t: number) => t,
'ease-in': (t: number) => t * t,
'ease-out': (t: number) => t * (2 - t),
'ease-in-out': (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
bounce: (t: number) => {
const n1 = 7.5625;
const d1 = 2.75;
if (t < 1 / d1) {
return n1 * t * t;
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
},
elastic: (t: number) => {
const c4 = (2 * Math.PI) / 3;
return t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
}
};
// パフォーマンス設定
const performanceSettings = {
high: {
throttle: 8, // 120fps
precision: 0.001
},
balanced: {
throttle: 16, // 60fps
precision: 0.01
},
smooth: {
throttle: 32, // 30fps
precision: 0.1
}
};
// 動的値の計算ヘルパー
const getDynamicValue = <T,>(
value: T | ((progress: number, data: DynamicData) => T) | ((data: DynamicData) => T),
progress: number,
data: DynamicData
): T => {
if (typeof value === 'function') {
try {
// 関数の引数の数で判断
if (value.length === 2) {
return (value as (progress: number, data: DynamicData) => T)(progress, data);
} else {
return (value as (data: DynamicData) => T)(data);
}
} catch {
return value as T;
}
}
return value as T;
};
// キーフレームアニメーションの計算
const calculateKeyframeAnimation = (
animation: DynamicAnimation,
progress: number,
timeElapsed: number
): Record<string, number> => {
const { keyframes, duration = 1000, repeat = false, direction = 'normal' } = animation;
if (keyframes.length === 0) return {};
// 時間ベースの進捗計算
let timeProgress = (timeElapsed % duration) / duration;
if (direction === 'reverse') {
timeProgress = 1 - timeProgress;
} else if (direction === 'alternate') {
const cycle = Math.floor(timeElapsed / duration) % 2;
timeProgress = cycle === 1 ? 1 - timeProgress : timeProgress;
}
// スクロール進捗と時間進捗の組み合わせ
const combinedProgress = (progress + timeProgress) / 2;
// キーフレーム間の補間
let currentFrame = keyframes[0];
let nextFrame = keyframes[0];
for (let i = 0; i < keyframes.length - 1; i++) {
if (combinedProgress >= keyframes[i].progress && combinedProgress <= keyframes[i + 1].progress) {
currentFrame = keyframes[i];
nextFrame = keyframes[i + 1];
break;
}
}
const frameProgress = (combinedProgress - currentFrame.progress) /
(nextFrame.progress - currentFrame.progress) || 0;
const result: Record<string, number> = {};
Object.keys(currentFrame.values).forEach(key => {
const currentValue = currentFrame.values[key] || 0;
const nextValue = nextFrame.values[key] || 0;
result[key] = currentValue + (nextValue - currentValue) * frameProgress;
});
return result;
};
/**
* 動的パララックス要素コンポーネント
*/
export function ParallaxElement({
id,
children,
speed = 0.5,
translateY = 0,
translateX = 0,
rotate = 0,
scale = 1,
opacity = 1,
blur = 0,
startOffset = 0,
endOffset = 1,
easing = 'linear',
dynamicAnimation,
dynamicData = {},
updateInterval = 100,
enable3D = true,
forceGPU = true,
className = '',
disabled = false,
onProgress,
onEnter,
onLeave,
onDataChange
}: ParallaxElementProps) {
const elementRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
const [scrollProgress, setScrollProgress] = useState(0);
const [currentData, setCurrentData] = useState<DynamicData>(dynamicData);
const [animationStartTime, setAnimationStartTime] = useState(Date.now());
const observerRef = useRef<IntersectionObserver | null>(null);
const animationFrameRef = useRef<number | null>(null);
const dataUpdateRef = useRef<NodeJS.Timeout | null>(null);
const lastScrollY = useRef(0);
const hasEntered = useRef(false);
// データ更新処理
useEffect(() => {
if (dynamicData && Object.keys(dynamicData).length > 0) {
const oldData = currentData;
setCurrentData(prev => ({ ...prev, ...dynamicData }));
if (onDataChange && JSON.stringify(oldData) !== JSON.stringify(dynamicData)) {
onDataChange(dynamicData, oldData);
}
}
}, [dynamicData, onDataChange]);
// リアルタイムデータ更新
useEffect(() => {
if (updateInterval > 0 && isVisible) {
dataUpdateRef.current = setInterval(() => {
// アニメーション開始時間の更新(キーフレームアニメーション用)
if (dynamicAnimation) {
setAnimationStartTime(Date.now());
}
}, updateInterval);
return () => {
if (dataUpdateRef.current) {
clearInterval(dataUpdateRef.current);
}
};
}
}, [updateInterval, isVisible, dynamicAnimation]);
// スクロール処理
const handleScroll = useCallback(() => {
if (!elementRef.current || disabled) return;
const element = elementRef.current;
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight;
// ビューポート内での進捗計算
const elementTop = rect.top;
const elementHeight = rect.height;
const startPosition = windowHeight * (1 - startOffset);
const endPosition = -elementHeight * endOffset;
const totalDistance = startPosition - endPosition;
const currentDistance = startPosition - elementTop;
let progress = totalDistance > 0 ? currentDistance / totalDistance : 0;
progress = Math.max(0, Math.min(1, progress));
// イージング適用
const easedProgress = easingFunctions[easing](progress);
setScrollProgress(easedProgress);
onProgress?.(easedProgress, element, currentData);
lastScrollY.current = window.scrollY;
}, [disabled, startOffset, endOffset, easing, onProgress, currentData]);
// パフォーマンス最適化されたスクロールハンドラー
const throttledScroll = useCallback(() => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
handleScroll();
});
}, [handleScroll]);
// Intersection Observer設定
useEffect(() => {
if (!elementRef.current || disabled) return;
observerRef.current = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const wasVisible = isVisible;
const nowVisible = entry.isIntersecting;
setIsVisible(nowVisible);
if (nowVisible && !hasEntered.current) {
hasEntered.current = true;
onEnter?.(entry.target as HTMLElement);
} else if (!nowVisible && hasEntered.current) {
hasEntered.current = false;
onLeave?.(entry.target as HTMLElement);
}
});
},
{
rootMargin: '50px',
threshold: 0
}
);
observerRef.current.observe(elementRef.current);
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [disabled, isVisible, onEnter, onLeave]);
// スクロールイベントリスナー
useEffect(() => {
if (!isVisible || disabled) return;
window.addEventListener('scroll', throttledScroll, { passive: true });
window.addEventListener('resize', throttledScroll, { passive: true });
// 初期実行
throttledScroll();
return () => {
window.removeEventListener('scroll', throttledScroll);
window.removeEventListener('resize', throttledScroll);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isVisible, disabled, throttledScroll]);
// 動的スタイル計算
const transformStyle = useMemo(() => {
if (disabled || !isVisible) {
return {};
}
const progress = scrollProgress;
// 動的速度の計算
const speedMultiplier = getDynamicValue(speed, progress, currentData);
// キーフレームアニメーションの計算
let keyframeValues: Record<string, number> = {};
if (dynamicAnimation) {
const timeElapsed = Date.now() - animationStartTime;
keyframeValues = calculateKeyframeAnimation(dynamicAnimation, progress, timeElapsed);
}
// 変換値の計算(動的値とキーフレーム値の組み合わせ)
const baseTranslateY = getDynamicValue(translateY, progress, currentData);
const baseTranslateX = getDynamicValue(translateX, progress, currentData);
const baseRotate = getDynamicValue(rotate, progress, currentData);
const baseScale = getDynamicValue(scale, progress, currentData);
const baseOpacity = getDynamicValue(opacity, progress, currentData);
const baseBlur = getDynamicValue(blur, progress, currentData);
const finalTranslateY = (baseTranslateY * progress * speedMultiplier) + (keyframeValues.translateY || 0);
const finalTranslateX = (baseTranslateX * progress * speedMultiplier) + (keyframeValues.translateX || 0);
const finalRotate = (baseRotate * progress) + (keyframeValues.rotate || 0);
const finalScale = (1 + (baseScale - 1) * progress) * (1 + (keyframeValues.scale || 0));
const finalOpacity = Math.max(0, Math.min(1,
(baseOpacity + (1 - baseOpacity) * (1 - progress)) * (1 + (keyframeValues.opacity || 0))
));
const finalBlur = (baseBlur * progress) + (keyframeValues.blur || 0);
// Transform文字列の構築
const transforms = [];
if (enable3D) {
transforms.push(`translate3d(${finalTranslateX}px, ${finalTranslateY}px, 0)`);
} else {
if (finalTranslateX !== 0 || finalTranslateY !== 0) {
transforms.push(`translate(${finalTranslateX}px, ${finalTranslateY}px)`);
}
}
if (finalRotate !== 0) {
transforms.push(`rotate(${finalRotate}deg)`);
}
if (finalScale !== 1) {
transforms.push(`scale(${finalScale})`);
}
const style: React.CSSProperties = {
transform: transforms.length > 0 ? transforms.join(' ') : undefined,
opacity: finalOpacity !== 1 ? finalOpacity : undefined,
filter: finalBlur > 0 ? `blur(${finalBlur}px)` : undefined,
};
// GPU加速
if (forceGPU && transforms.length > 0) {
style.willChange = 'transform, opacity, filter';
style.backfaceVisibility = 'hidden';
style.perspective = '1000px';
}
return style;
}, [
disabled,
isVisible,
scrollProgress,
currentData,
animationStartTime,
speed,
translateY,
translateX,
rotate,
scale,
opacity,
blur,
dynamicAnimation,
enable3D,
forceGPU
]);
// 動的コンテンツのレンダリング
const renderedChildren = useMemo(() => {
if (typeof children === 'function') {
return children(currentData, scrollProgress);
}
return children;
}, [children, currentData, scrollProgress]);
return (
<div
ref={elementRef}
id={id}
className={className}
style={transformStyle}
>
{renderedChildren}
</div>
);
}
/**
* 動的パララックスコンテナコンポーネント
*/
export default function ParallaxContainer({
children,
sensitivity = 1,
performance = 'balanced',
debug = false,
globalData = {},
dataProvider,
dataUpdateInterval = 1000,
realtime = false,
className = '',
disabled = false,
onGlobalDataChange
}: ParallaxContainerProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollInfo, setScrollInfo] = useState({
scrollY: 0,
scrollDirection: 'down' as 'up' | 'down',
scrollSpeed: 0
});
const [currentGlobalData, setCurrentGlobalData] = useState<DynamicData>(globalData);
const settings = performanceSettings[performance];
const lastScrollY = useRef(0);
const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
const dataProviderRef = useRef<NodeJS.Timeout | null>(null);
// データプロバイダーの実行
const fetchDataFromProvider = useCallback(async () => {
if (!dataProvider) return;
try {
const newData = await Promise.resolve(dataProvider());
const oldData = currentGlobalData;
setCurrentGlobalData(prev => ({ ...prev, ...newData }));
if (onGlobalDataChange && JSON.stringify(oldData) !== JSON.stringify(newData)) {
onGlobalDataChange(newData, oldData);
}
} catch (error) {
console.warn('Data provider error:', error);
}
}, [dataProvider, currentGlobalData, onGlobalDataChange]);
// グローバルデータの更新
useEffect(() => {
if (globalData && Object.keys(globalData).length > 0) {
const oldData = currentGlobalData;
setCurrentGlobalData(prev => ({ ...prev, ...globalData }));
if (onGlobalDataChange && JSON.stringify(oldData) !== JSON.stringify(globalData)) {
onGlobalDataChange(globalData, oldData);
}
}
}, [globalData, onGlobalDataChange]);
// データプロバイダーの定期実行
useEffect(() => {
if (dataProvider && (realtime || dataUpdateInterval > 0)) {
// 初回実行
fetchDataFromProvider();
// 定期実行
if (dataUpdateInterval > 0) {
dataProviderRef.current = setInterval(fetchDataFromProvider, dataUpdateInterval);
}
return () => {
if (dataProviderRef.current) {
clearInterval(dataProviderRef.current);
}
};
}
}, [dataProvider, realtime, dataUpdateInterval, fetchDataFromProvider]);
// スクロール情報の更新
const updateScrollInfo = useCallback(() => {
const currentScrollY = window.scrollY;
const deltaY = currentScrollY - lastScrollY.current;
const direction = deltaY > 0 ? 'down' : 'up';
const speed = Math.abs(deltaY);
setScrollInfo({
scrollY: currentScrollY,
scrollDirection: direction,
scrollSpeed: speed
});
lastScrollY.current = currentScrollY;
}, []);
// パフォーマンス最適化されたスクロールハンドラー
const handleScroll = useCallback(() => {
if (disabled) return;
if (scrollTimeout.current) {
clearTimeout(scrollTimeout.current);
}
scrollTimeout.current = setTimeout(() => {
updateScrollInfo();
}, settings.throttle);
}, [disabled, settings.throttle, updateScrollInfo]);
// スクロールイベントリスナー
useEffect(() => {
if (disabled) return;
window.addEventListener('scroll', handleScroll, { passive: true });
// 初期実行
updateScrollInfo();
return () => {
window.removeEventListener('scroll', handleScroll);
if (scrollTimeout.current) {
clearTimeout(scrollTimeout.current);
}
};
}, [disabled, handleScroll, updateScrollInfo]);
// デバッグ情報
const debugInfo = debug ? (
<div
style={{
position: 'fixed',
top: '10px',
right: '10px',
background: 'rgba(0, 0, 0, 0.8)',
color: 'white',
padding: '10px',
borderRadius: '4px',
fontSize: '12px',
fontFamily: 'monospace',
zIndex: 9999,
pointerEvents: 'none',
maxWidth: '300px',
maxHeight: '400px',
overflow: 'auto'
}}
>
<div>📊 スクロール情報</div>
<div>Scroll Y: {scrollInfo.scrollY}</div>
<div>Direction: {scrollInfo.scrollDirection}</div>
<div>Speed: {scrollInfo.scrollSpeed}</div>
<div>Performance: {performance}</div>
<div>Sensitivity: {sensitivity}</div>
<div style={{ marginTop: '10px' }}>🔄 動的データ</div>
<div>Realtime: {realtime ? 'ON' : 'OFF'}</div>
<div>Update Interval: {dataUpdateInterval}ms</div>
<div>Data Provider: {dataProvider ? 'Active' : 'None'}</div>
{Object.keys(currentGlobalData).length > 0 && (
<div style={{ marginTop: '10px' }}>
<div>Global Data:</div>
<pre style={{ fontSize: '10px', margin: '5px 0', wordWrap: 'break-word' }}>
{JSON.stringify(currentGlobalData, null, 2)}
</pre>
</div>
)}
</div>
) : null;
return (
<div
ref={containerRef}
className={className}
style={{
position: 'relative',
...(disabled ? {} : { overflow: 'hidden' })
}}
>
{children}
{debugInfo}
</div>
);
}
// 便利なプリセットコンポーネント
/**
* パララックス背景画像
*/
export interface ParallaxBackgroundProps {
/** 背景画像URL */
src: string;
/** 代替テキスト */
alt?: string;
/** パララックス速度 */
speed?: number;
/** 高さ */
height?: string;
/** カバーモード */
objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
/** カスタムクラス */
className?: string;
}
export function ParallaxBackground({
src,
alt = '',
speed = 0.5,
height = '100vh',
objectFit = 'cover',
className = ''
}: ParallaxBackgroundProps) {
return (
<div
className={`relative overflow-hidden ${className}`}
style={{ height }}
>
<ParallaxElement
speed={speed}
translateY={100}
className="absolute inset-0 w-full h-full"
>
<img
src={src}
alt={alt}
className="w-full h-full"
style={{
objectFit,
transform: 'scale(1.1)' // 隙間防止
}}
/>
</ParallaxElement>
</div>
);
}
/**
* パララックステキスト
*/
export interface ParallaxTextProps {
/** テキスト内容 */
children: React.ReactNode;
/** パララックス速度 */
speed?: number;
/** フォントサイズ */
fontSize?: string;
/** テキスト色 */
color?: string;
/** アニメーション効果 */
effect?: 'fade' | 'scale' | 'blur' | 'rotate';
/** カスタムクラス */
className?: string;
}
export function ParallaxText({
children,
speed = 0.3,
fontSize = '2rem',
color = 'inherit',
effect = 'fade',
className = ''
}: ParallaxTextProps) {
const effectProps = useMemo(() => {
switch (effect) {
case 'fade':
return { opacity: 0.3, speed };
case 'scale':
return { scale: 1.5, speed };
case 'blur':
return { blur: 5, speed };
case 'rotate':
return { rotate: 45, speed };
default:
return { speed };
}
}, [effect, speed]);
return (
<ParallaxElement
{...effectProps}
className={className}
>
<div
style={{
fontSize,
color,
textAlign: 'center'
}}
>
{children}
</div>
</ParallaxElement>
);
}
/**
* レイヤードパララックス
*/
export interface ParallaxLayerProps {
/** レイヤー要素 */
layers: Array<{
content: React.ReactNode;
speed: number;
zIndex?: number;
className?: string;
}>;
/** コンテナの高さ */
height?: string;
/** カスタムクラス */
className?: string;
}
export function ParallaxLayers({
layers,
height = '100vh',
className = ''
}: ParallaxLayerProps) {
return (
<div
className={`relative ${className}`}
style={{ height }}
>
{layers.map((layer, index) => (
<ParallaxElement
key={index}
speed={layer.speed}
translateY={50}
className={`absolute inset-0 ${layer.className || ''}`}
style={{ zIndex: layer.zIndex || index }}
>
{layer.content}
</ParallaxElement>
))}
</div>
);
}
// 動的コンテンツ用のプリセットコンポーネント
/**
* 動的カウンターパララックス
*/
export interface DynamicCounterProps {
/** 開始値 */
startValue?: number;
/** 終了値または関数 */
endValue: number | ((data: DynamicData) => number);
/** カウンター形式 */
format?: (value: number) => string;
/** パララックス速度 */
speed?: number;
/** カスタムクラス */
className?: string;
/** 動的データ */
dynamicData?: DynamicData;
}
export function DynamicCounter({
startValue = 0,
endValue,
format = (value) => Math.round(value).toString(),
speed = 0.5,
className = '',
dynamicData = {}
}: DynamicCounterProps) {
return (
<ParallaxElement
speed={speed}
dynamicData={dynamicData}
className={className}
>
{(data, progress) => {
const target = typeof endValue === 'function' ? endValue(data) : endValue;
const currentValue = startValue + (target - startValue) * progress;
return (
<div className="text-4xl font-bold">
{format(currentValue)}
</div>
);
}}
</ParallaxElement>
);
}
/**
* 動的プログレスバーパララックス
*/
export interface DynamicProgressProps {
/** プログレス値または関数 */
value: number | ((data: DynamicData, progress: number) => number);
/** 最大値 */
max?: number;
/** 色設定 */
color?: string | ((data: DynamicData) => string);
/** パララックス速度 */
speed?: number;
/** カスタムクラス */
className?: string;
/** 動的データ */
dynamicData?: DynamicData;
}
export function DynamicProgress({
value,
max = 100,
color = '#3b82f6',
speed = 0.3,
className = '',
dynamicData = {}
}: DynamicProgressProps) {
return (
<ParallaxElement
speed={speed}
dynamicData={dynamicData}
className={`w-full bg-gray-200 rounded-full h-4 ${className}`}
>
{(data, progress) => {
const currentValue = typeof value === 'function' ? value(data, progress) : value;
const currentColor = typeof color === 'function' ? color(data) : color;
const percentage = Math.min(100, (currentValue / max) * 100);
return (
<div className="relative w-full bg-gray-200 rounded-full h-4 overflow-hidden">
<div
className="h-full rounded-full transition-all duration-300"
style={{
width: `${percentage}%`,
backgroundColor: currentColor
}}
/>
<div className="absolute inset-0 flex items-center justify-center text-sm font-medium text-white">
{Math.round(percentage)}%
</div>
</div>
);
}}
</ParallaxElement>
);
}
/**
* 動的チャートパララックス
*/
export interface DynamicChartProps {
/** データまたは関数 */
data: number[] | ((data: DynamicData, progress: number) => number[]);
/** チャートタイプ */
type?: 'bar' | 'line' | 'area';
/** カラーテーマ */
colors?: string[];
/** 高さ */
height?: number;
/** パララックス速度 */
speed?: number;
/** カスタムクラス */
className?: string;
/** 動的データ */
dynamicData?: DynamicData;
}
export function DynamicChart({
data,
type = 'bar',
colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'],
height = 200,
speed = 0.4,
className = '',
dynamicData = {}
}: DynamicChartProps) {
return (
<ParallaxElement
speed={speed}
dynamicData={dynamicData}
className={className}
>
{(dynamicDataValue, progress) => {
const currentData = typeof data === 'function' ? data(dynamicDataValue, progress) : data;
const maxValue = Math.max(...currentData);
return (
<div className="w-full" style={{ height }}>
<div className="flex items-end justify-center h-full space-x-2 p-4">
{currentData.map((value, index) => {
const barHeight = (value / maxValue) * (height - 32);
const color = colors[index % colors.length];
return (
<div
key={index}
className="flex-1 max-w-12 rounded-t-lg transition-all duration-500"
style={{
height: barHeight,
backgroundColor: color,
opacity: 0.3 + (progress * 0.7)
}}
/>
);
})}
</div>
</div>
);
}}
</ParallaxElement>
);
}
/**
* リアルタイムデータプロバイダー(サンプル)
*/
export const createRealtimeDataProvider = (baseUrl?: string) => {
return async (): Promise<DynamicData> => {
try {
if (baseUrl) {
const response = await fetch(baseUrl);
return await response.json();
} else {
// モックデータ
return {
timestamp: Date.now(),
userCount: Math.floor(Math.random() * 1000) + 100,
revenue: Math.floor(Math.random() * 50000) + 10000,
conversionRate: (Math.random() * 5 + 2).toFixed(2),
activeUsers: Math.floor(Math.random() * 50) + 10,
temperature: (Math.random() * 30 + 10).toFixed(1),
serverLoad: Math.floor(Math.random() * 100),
chartData: Array.from({ length: 7 }, () => Math.floor(Math.random() * 100) + 20)
};
}
} catch (error) {
console.warn('Failed to fetch realtime data:', error);
return {};
}
};
};
// 動的パララックスの使用例
import ParallaxContainer, {
ParallaxElement,
DynamicCounter,
DynamicProgress,
DynamicChart,
createRealtimeDataProvider
} from './scroll-parallax';
function App() {
// リアルタイムデータプロバイダーの作成
const dataProvider = createRealtimeDataProvider('/api/dashboard');
return (
<ParallaxContainer
performance="balanced"
dataProvider={dataProvider}
dataUpdateInterval={2000}
realtime={true}
debug={true}
onGlobalDataChange={(newData, oldData) => {
console.log('グローバルデータ更新:', newData);
}}
>
{/* 動的カウンター */}
<DynamicCounter
startValue={0}
endValue={(data) => data.userCount || 500}
format={(value) => `${Math.round(value).toLocaleString()}人`}
speed={0.4}
className="text-center py-20"
/>
{/* データドリブンなパララックス要素 */}
<ParallaxElement
speed={(data) => data.serverLoad > 80 ? 0.8 : 0.3}
translateY={(progress, data) => {
const load = data.serverLoad || 50;
return 100 + (load / 100) * 50;
}}
rotate={(progress, data) => {
return (data.temperature || 20) * progress;
}}
opacity={(progress, data) => {
const isAlert = data.serverLoad > 90;
return isAlert ? 0.5 + Math.sin(Date.now() / 200) * 0.3 : 1;
}}
onProgress={(progress, element, data) => {
if (data.serverLoad > 90) {
element.style.backgroundColor = '#ef4444';
}
}}
>
{(data, progress) => (
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-8 rounded-xl text-white">
<h2 className="text-2xl font-bold mb-4">
サーバー負荷: {data.serverLoad || '--'}%
</h2>
<p>温度: {data.temperature || '--'}°C</p>
<p>スクロール進捗: {Math.round(progress * 100)}%</p>
</div>
)}
</ParallaxElement>
{/* 動的プログレスバー */}
<DynamicProgress
value={(data, progress) => (data.conversionRate || 2.5) * progress * 20}
max={100}
color={(data) => data.conversionRate > 4 ? '#10b981' : '#f59e0b'}
speed={0.2}
className="mb-8"
/>
{/* リアルタイムチャート */}
<DynamicChart
data={(data, progress) => {
const baseData = data.chartData || [20, 30, 25, 40, 35, 50, 45];
return baseData.map(value => value * (0.5 + progress * 0.5));
}}
type="bar"
height={200}
speed={0.3}
colors={['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6']}
/>
{/* キーフレームアニメーション */}
<ParallaxElement
speed={0.5}
dynamicAnimation={{
keyframes: [
{ progress: 0, values: { rotate: 0, scale: 1 } },
{ progress: 0.5, values: { rotate: 180, scale: 1.5 } },
{ progress: 1, values: { rotate: 360, scale: 1 } }
],
duration: 3000,
direction: 'alternate'
}}
updateInterval={50}
>
<div className="w-20 h-20 bg-gradient-to-r from-pink-400 to-red-500 rounded-full mx-auto" />
</ParallaxElement>
{/* 条件付きレンダリング */}
<ParallaxElement speed={0.4}>
{(data, progress) => {
const isHighTraffic = data.activeUsers > 30;
return (
<div className={`transition-all duration-500 ${
isHighTraffic ? 'bg-green-500' : 'bg-gray-500'
} p-8 rounded-lg text-white text-center`}>
<h3 className="text-xl font-bold mb-2">
{isHighTraffic ? '🔥 高トラフィック!' : '📊 通常運用中'}
</h3>
<p>アクティブユーザー: {data.activeUsers || '--'}人</p>
<p>収益: ¥{(data.revenue || 0).toLocaleString()}</p>
</div>
);
}}
</ParallaxElement>
</ParallaxContainer>
);
}