rigid 600ms · elastic 400ms · gas 1000ms

Material Easing

モーションを素材感(金属・ゴム・水・紙・煙・ガラス)で捉える。同じ移動でも質感パラメータで体感がまったく変わる。

rigid 600ms · elastic 400ms · gas 1000ms

Rigid

金属・石

重く、跳ね返らない

600ms

Elastic

ゴム・ゲル

オーバーシュート、跳ねる

400ms

Fluid

水・絵具

ゆったり、わずかに揺れる

750ms

Paper

紙・カード

標準。軽くもなく重くもなく

500ms

Gas

煙・霧

とても遅く、形が散る

1000ms

Glass

ガラス

鋭く止まる、もろい

450ms
/**
 * Material Easing — 素材感(金属・ゴム・水・紙・煙・ガラス)で easing を選ぶ。
 * 移植元: motion-playground app/_sections/MaterialEasing.tsx(dark 変換 M-B-2)
 */
import { motion } from 'motion/react';
import { materials, type MaterialKey } from '../../../lib/motion-tokens/materials';
import { useLabReplay } from './use-lab-replay';
import { useTrackWidth } from './use-track-width';

const order: MaterialKey[] = [
  'rigid',
  'elastic',
  'fluid',
  'paper',
  'gas',
  'glass',
];

const BASE_DURATION = 0.5;

function MaterialLane({
  matKey,
  playKey,
}: {
  matKey: MaterialKey;
  playKey: number;
}) {
  const m = materials[matKey];
  const duration = BASE_DURATION * m.durationScale;
  const overshootScale = 1 + m.overshoot;
  const [trackRef, travel] = useTrackWidth(32, 16);

  return (
    <div className="flex items-center gap-4 border-b border-[var(--border-subtle)] py-3 last:border-none">
      <div className="w-40 shrink-0">
        <p className="text-sm font-medium text-[var(--text-primary)]">{m.label}</p>
        <p className="text-xs text-[var(--text-muted)]">{m.jp}</p>
        <p className="mt-0.5 text-[11px] text-[var(--text-muted)]">{m.note}</p>
      </div>
      <div
        ref={trackRef}
        className="relative h-12 flex-1 overflow-hidden rounded-md bg-[var(--bg-elevated)]"
      >
        <motion.div
          key={playKey}
          initial={{ x: 0, scale: 1 }}
          animate={
            m.overshoot > 0
              ? {
                  x: [0, travel, travel],
                  scale: [1, overshootScale, 1],
                }
              : {
                  x: [0, travel],
                  scale: matKey === 'gas' ? [1, 1.4] : [1, 1],
                }
          }
          transition={{
            duration,
            ease: m.ease,
            times: m.overshoot > 0 ? [0, 0.75, 1] : [0, 1],
          }}
          className={`absolute top-2 left-2 h-8 w-8 rounded-md ${m.swatch} ${
            matKey === 'gas' ? 'opacity-60 blur-[2px]' : ''
          }`}
        />
      </div>
      <span className="w-12 shrink-0 font-mono text-[11px] text-[var(--text-muted)]">
        {Math.round(duration * 1000)}ms
      </span>
    </div>
  );
}

export default function MaterialEasing() {
  const { rootRef, playKey } = useLabReplay();

  return (
    <div ref={rootRef} data-lab-island className="w-full p-4 lg:p-6">
      <div className="rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-surface)] px-4 py-2">
        {order.map((k) => (
          <MaterialLane key={k} matKey={k} playKey={playKey} />
        ))}
      </div>
    </div>
  );
}