6 presets · 160–950ms · bounce / gentle / accelerate

Emotion to Motion

喜び・穏やかさ・緊急・悲しみ・驚き・気品の 6 感情を duration・easing・移動・回転・scale の組み合わせで表現する。同じ「丸が登場する」動きでもパラメータだけで感情が変わる。

6 presets · 160–950ms · bounce / gentle / accelerate

※ カードをクリックすると再生される

// notes

  • duration と easing の組み合わせだけで「喜び」と「悲しみ」のような対照的な感情を表現できる。速く弾む = 喜び、遅く重力に従う = 悲しみ。
  • overshoot(bounce-settle)は playful な質感を生むが、使いすぎると軽薄に見える。
  • travel の x / y 方向は感情の「出どころ」を暗示する。urgency は横から割り込ませると緊迫感が出る。
  • scaleFrom の初期値が「驚き」と「穏やかさ」の差を生む。極端に小さい初期 scale は pop-in の驚き、1 に近い初期 scale は落ち着き。
/**
 * Emotion → Motion — 感情プリセットを duration / easing / travel path に翻訳する。
 * クリックで個別再生(stage の Replay ではなく操作駆動なので useLabReplay は使わない)。
 * 移植元: motion-playground app/_sections/EmotionToMotion.tsx(dark 変換 M-B-2)
 */
import { motion } from 'motion/react';
import { useState } from 'react';
import { emotions, type EmotionKey } from '../../../lib/motion-tokens/playground';

const order: EmotionKey[] = [
  'joy',
  'calm',
  'urgency',
  'sadness',
  'surprise',
  'elegance',
];

function EmotionTile({ emotionKey }: { emotionKey: EmotionKey }) {
  const e = emotions[emotionKey];
  const [playKey, setPlayKey] = useState(0);

  return (
    <button
      type="button"
      onClick={() => setPlayKey((k) => k + 1)}
      className={`group relative h-44 overflow-hidden rounded-lg bg-gradient-to-br ${e.accent} text-left transition-shadow hover:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-strong)]`}
    >
      <div className="absolute inset-0 flex items-center justify-center">
        <motion.div
          key={playKey}
          initial={{
            x: e.travel.x,
            y: e.travel.y,
            rotate: e.rotate,
            scale: e.scaleFrom,
            opacity: 0,
          }}
          animate={{
            x: 0,
            y: 0,
            rotate: 0,
            scale: 1,
            opacity: 1,
          }}
          transition={{ duration: e.duration, ease: e.ease }}
          className="h-12 w-12 rounded-full bg-[var(--bg-surface)]/90 shadow-sm ring-1 ring-[var(--border-subtle)]"
        />
      </div>
      <div className="absolute inset-x-0 bottom-0 flex items-end justify-between p-4">
        <div>
          <p className="text-sm font-semibold text-[var(--text-primary)]">
            {e.label}
          </p>
          <p className="text-xs text-[var(--text-secondary)]">{e.jp}</p>
        </div>
        <span className="font-mono text-[11px] text-[var(--text-muted)]">
          {Math.round(e.duration * 1000)}ms
        </span>
      </div>
    </button>
  );
}

export default function EmotionToMotion() {
  return (
    <div data-lab-island className="w-full p-4 lg:p-6">
      <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
        {order.map((key) => (
          <EmotionTile key={key} emotionKey={key} />
        ))}
      </div>
      <p className="mt-4 text-xs text-[var(--text-muted)]">
        ※ カードをクリックすると再生される
      </p>
    </div>
  );
}