動的スクロールパララックス - Vibe Coding Showcase

動的スクロールパララックス

スクロール連動で要素に視差効果を適用する高性能パララックスコンポーネント。リアルタイムデータ連携、動的要素生成、カスタムアニメーション制御に対応。Intersection Observer APIとRequestAnimationFrameで最適化されたアニメーション

デザインプレビュー

背景レイヤー(遅い)

スクロールパララックス

スクロールすると、各レイヤーが異なる速度で動きます

スクロール連動フェードイン

要素 1

スクロールに応じて透明度と位置が変化します

要素 2

画面に入ると徐々に表示されます

要素 3

スムーズなトランジション効果

フェードイン効果

フェードイン要素 1

スクロールで表示されます

フェードイン要素 2

スクロールで表示されます

フェードイン要素 3

スクロールで表示されます

スクロール連動アニメーション

回転

スクロールで回転

浮遊

スクロールで上下

脈動

スクロールで拡大縮小

動的コンテンツの例

0

訪問者数カウンター

進捗: 0%

売上推移チャート

動的スクロールパララックスの特徴

高性能スクロール追従

Intersection Observer APIとRequestAnimationFrameによる最適化で、滑らかで軽量なパララックス効果を実現

動的コンテンツ対応

リアルタイムデータ連携、動的要素生成、関数ベースの値計算により、データドリブンなパララックス体験を実現

豊富な変換オプション

移動、回転、スケール、透明度、ぼかし効果など多彩な変換を組み合わせた複雑なアニメーション。キーフレームアニメーション対応

プリセットコンポーネント

パララックス背景、動的カウンター、リアルタイムチャート、プログレスバーなど即座に使える動的プリセット

AI活用ガイド

リアルタイムダッシュボードパララックス

プロンプト例:

リアルタイムデータに基づいて変化する動的パララックスダッシュボードを作成してください。API連携によるデータ取得、動的カウンター、プログレスバー、チャートのアニメーション、データ変化に応じた色彩変化、アラート表示システムを実装してください。

インタラクティブストーリーテリング

プロンプト例:

スクロールで進行するインタラクティブなストーリー体験を作成してください。動的コンテンツ生成によるキャラクター変化、章ごとのパララックス背景変化、リアルタイム選択肢分岐、ユーザー行動に応じたストーリー展開、進捗同期型音響効果を含めてください。

ゲーミフィケーションパララックス

プロンプト例:

ゲーム要素を取り入れた動的パララックス体験を開発してください。ユーザーアクションによるスコア計算、リアルタイムランキング、実績システム、スクロール連動レベルアップ、動的報酬表示、ソーシャル機能連携を実装してください。

実装コード

ファイルサイズ: 28.4KB TypeScript

コンポーネント実装

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>
  );
}