SSGOI

バウンスアニメーション

要素が弾むような生き生きとした効果を作ります

バウンスアニメーション

バウンス(Bounce)アニメーションは、要素が弾み跳ねるような効果を作り出し、楽しく生き生きとしたインタラクションを提供します。注目を集めたり、楽しいユーザー体験を作る際に効果的です。

基本的な使い方

import { transition } from '@ssgoi/react';
import { bounce } from '@ssgoi/react/transitions';

function Component() {
  const [isVisible, setIsVisible] = useState(true);
  
  return (
    <div>
      {isVisible && (
        <div ref={transition({ key: 'bounce-element', ...bounce() })}>
          バウンスアニメーションが適用された要素
        </div>
      )}
    </div>
  );
}

オプション

interface BounceOptions {
  height?: number;         // バウンスの高さ(デフォルト:20)
  intensity?: number;      // バウンスの強度(デフォルト:1)
  scale?: boolean;         // スケール効果追加(デフォルト:true)
  fade?: boolean;          // フェード効果追加(デフォルト:false)
  direction?: 'up' | 'down';  // バウンス方向(デフォルト:'up')
  spring?: {
    stiffness?: number;    // スプリング強度(デフォルト:800)
    damping?: number;      // 減衰係数(デフォルト:15)
  };
}

オプション説明

  • height: バウンス最大高さ(ピクセル)
  • intensity: バウンス回数と強度(1 = 基本、2 = 倍)
  • scale: バウンスと一緒にサイズ変化効果
  • fade: バウンスと一緒にフェード効果
  • direction: バウンス方向
    • 'up': 上に弾む
    • 'down': 下に落ちる
  • spring: スプリング物理設定(高いstiffness = 速いバウンス)

使用例

バウンス強度の調整

// ソフトなバウンス
const softBounce = bounce({ 
  height: 10,
  intensity: 0.5,
  spring: { stiffness: 600, damping: 20 }
});

// 強いバウンス
const strongBounce = bounce({ 
  height: 30,
  intensity: 2,
  spring: { stiffness: 1000, damping: 10 }
});

// 微細なバウンス
const subtleBounce = bounce({ 
  height: 5,
  intensity: 0.3
});

方向変更

// 上にバウンス(デフォルト)
const bounceUp = bounce({ 
  direction: 'up' 
});

// 下にバウンス(落下効果)
const bounceDown = bounce({ 
  direction: 'down',
  height: 25
});

複合効果

// バウンス + フェード
const bounceFade = bounce({ 
  fade: true,
  height: 20
});

// バウンスのみ(スケールなし)
const bounceOnly = bounce({ 
  scale: false,
  height: 15
});

// すべての効果を組み合わせ
const bounceAll = bounce({ 
  height: 25,
  intensity: 1.5,
  scale: true,
  fade: true,
  spring: { stiffness: 700, damping: 12 }
});

実用的な活用例

いいねボタン

function LikeButton() {
  const [isLiked, setIsLiked] = useState(false);
  
  return (
    <button
      onClick={() => setIsLiked(!isLiked)}
      className="relative p-2"
    >
      {isLiked ? (
        <HeartIcon 
          ref={transition({ 
            key: 'heart-filled', 
            ...bounce({ 
              height: 15,
              intensity: 1.2,
              spring: { stiffness: 900, damping: 15 }
            }) 
          })}
          className="w-8 h-8 text-red-500"
        />
      ) : (
        <HeartOutlineIcon className="w-8 h-8 text-gray-400" />
      )}
      
      {/* いいねカウントアニメーション */}
      {isLiked && (
        <span 
          ref={transition({ 
            key: 'like-count', 
            ...bounce({ 
              height: 10,
              direction: 'up',
              fade: true
            }) 
          })}
          className="absolute -top-2 -right-2 text-xs text-red-500"
        >
          +1
        </span>
      )}
    </button>
  );
}

通知ベル

function NotificationBell({ hasNew }) {
  return (
    <div className="relative">
      <BellIcon 
        ref={hasNew ? transition({ 
          key: 'bell-bounce', 
          ...bounce({ 
            height: 8,
            intensity: 2,
            spring: { stiffness: 1200, damping: 20 }
          }) 
        }) : undefined}
        className="w-6 h-6"
      />
      
      {hasNew && (
        <div 
          ref={transition({ 
            key: 'notification-dot', 
            ...bounce({ 
              height: 5,
              scale: true
            }) 
          })}
          className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full"
        />
      )}
    </div>
  );
}

成功メッセージ

function SuccessMessage({ show, message }) {
  return (
    <>
      {show && (
        <div 
          ref={transition({ 
            key: 'success-message', 
            ...bounce({ 
              height: 20,
              direction: 'down',
              fade: true,
              spring: { stiffness: 600, damping: 18 }
            }) 
          })}
          className="fixed top-4 left-1/2 -translate-x-1/2 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center gap-2"
        >
          <CheckCircleIcon className="w-5 h-5" />
          {message}
        </div>
      )}
    </>
  );
}

ドロップアニメーション

function DroppableItem({ item, onDrop }) {
  const [isDragging, setIsDragging] = useState(false);
  const [isDropped, setIsDropped] = useState(false);
  
  const handleDrop = () => {
    setIsDropped(true);
    onDrop(item);
  };
  
  return (
    <div
      draggable
      onDragStart={() => setIsDragging(true)}
      onDragEnd={() => {
        setIsDragging(false);
        handleDrop();
      }}
      className={isDragging ? 'opacity-50' : ''}
    >
      {isDropped ? (
        <div 
          ref={transition({ 
            key: 'dropped-item', 
            ...bounce({ 
              height: 30,
              direction: 'down',
              intensity: 1.5,
              spring: { stiffness: 500, damping: 12 }
            }) 
          })}
          className="p-4 bg-blue-100 rounded-lg"
        >
          {item.name} (ドロップ済み!)
        </div>
      ) : (
        <div className="p-4 bg-gray-100 rounded-lg cursor-move">
          {item.name}
        </div>
      )}
    </div>
  );
}

高度な活用

連続バウンス

function ContinuousBounce() {
  const [bounceCount, setBounceCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setBounceCount(c => c + 1);
    }, 2000);
    
    return () => clearInterval(interval);
  }, []);
  
  return (
    <div 
      ref={transition({ 
        key: `bounce-${bounceCount}`, 
        ...bounce({ 
          height: 15,
          intensity: 0.8
        }) 
      })}
      className="w-20 h-20 bg-purple-500 rounded-full"
    />
  );
}

弾性メニュー

function ElasticMenu({ items }) {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <div className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="p-3 bg-blue-500 text-white rounded-full"
      >
        <MenuIcon />
      </button>
      
      {isOpen && (
        <div className="absolute top-full mt-2 space-y-2">
          {items.map((item, index) => (
            <button
              key={item.id}
              ref={transition({ 
                key: `menu-item-${item.id}`, 
                ...bounce({ 
                  height: 20 - index * 3,  // 段階的に減少
                  intensity: 1,
                  spring: { 
                    stiffness: 800 - index * 50,  // 順次効果
                    damping: 15 
                  }
                }) 
              })}
              className="block w-full px-4 py-2 bg-white rounded-lg shadow hover:bg-gray-50"
              style={{ 
                transitionDelay: `${index * 50}ms`  // スタガー効果
              }}
            >
              {item.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

ボール弾みシミュレーション

function BouncingBall() {
  const [position, setPosition] = useState({ x: 50, y: 0 });
  const [bounceKey, setBounceKey] = useState(0);
  
  const handleClick = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    setPosition({
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    });
    setBounceKey(k => k + 1);
  };
  
  return (
    <div 
      className="relative w-full h-96 bg-gray-100 overflow-hidden"
      onClick={handleClick}
    >
      <div
        ref={transition({ 
          key: `ball-${bounceKey}`, 
          ...bounce({ 
            height: 150,
            direction: 'down',
            intensity: 2,
            scale: true,
            spring: { stiffness: 400, damping: 10 }
          }) 
        })}
        className="absolute w-12 h-12 bg-red-500 rounded-full"
        style={{ 
          left: position.x - 24,
          top: position.y - 24
        }}
      />
    </div>
  );
}

パフォーマンス最適化

  • バウンスはtransform: translateY()を使用してGPUアクセラレーションされます
  • 高いintensity値はより多くの計算を必要とします
  • 複数要素の同時バウンスはパフォーマンスを考慮する必要があります

パフォーマンスのヒント

// モバイル最適化
const isMobile = window.innerWidth < 768;
const optimizedBounce = bounce({
  height: isMobile ? 10 : 20,
  intensity: isMobile ? 0.8 : 1,
  spring: { 
    stiffness: isMobile ? 900 : 800,
    damping: 15 
  }
});

アクセシビリティの考慮事項

<button
  ref={transition({ 
    key: 'accessible-bounce', 
    ...bounce() 
  })}
  aria-label="新しい通知があります"
  aria-live="polite"
  aria-atomic="true"
>
  <NotificationIcon />
</button>

推奨される使用例

  • インタラクションフィードバック: ボタンクリック、いいね
  • 通知: 新着メッセージ、アップデート通知
  • 成功/完了状態: タスク完了表示
  • ゲーム要素: スコア獲得、アイテム収集
  • チュートリアル: 注目すべき要素の強調