linear · arc · s-curve · spiral · 1.4s

Path Types

同じ A → B の移動でも経路(直線・アーク・S 字・スパイラル)で体感がまったく変わることを CSS Motion Path で比較する。

linear · arc · s-curve · spiral · 1.4s

Linear

直線

機械的・purposeful

Arc

アーク

親しみやすい・joyful

S-curve

S字

fluid・elegant

Spiral

スパイラル

whimsical・playful

/**
 * Path Types — A→B の移動経路(直線・アーク・S字・スパイラル)で感情が変わることを比較する。
 * 移植元: motion-playground app/_sections/PathTypes.tsx(dark 変換 M-B-2)
 */
import { motion } from 'motion/react';
import { easings } from '../../../lib/motion-tokens/playground';
import { useLabReplay } from './use-lab-replay';

const paths: {
  key: string;
  label: string;
  jp: string;
  path: string;
  note: string;
}[] = [
  {
    key: 'line',
    label: 'Linear',
    jp: '直線',
    path: 'M 20 60 L 320 60',
    note: '機械的・purposeful',
  },
  {
    key: 'arc',
    label: 'Arc',
    jp: 'アーク',
    path: 'M 20 60 Q 170 -10 320 60',
    note: '親しみやすい・joyful',
  },
  {
    key: 'sine',
    label: 'S-curve',
    jp: 'S字',
    path: 'M 20 60 C 100 0, 240 120, 320 60',
    note: 'fluid・elegant',
  },
  {
    key: 'spiral',
    label: 'Spiral',
    jp: 'スパイラル',
    path: 'M 320 60 C 280 -40, 80 120, 60 40 S 240 100, 320 60',
    note: 'whimsical・playful',
  },
];

function PathLane({
  label,
  jp,
  note,
  path,
  playKey,
}: {
  label: string;
  jp: string;
  note: string;
  path: string;
  playKey: number;
}) {
  return (
    <div className="rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-surface)] p-4">
      <div className="mb-2 flex items-baseline justify-between">
        <div>
          <p className="text-sm font-medium text-[var(--text-primary)]">{label}</p>
          <p className="text-xs text-[var(--text-muted)]">{jp}</p>
        </div>
        <p className="text-[11px] text-[var(--text-muted)]">{note}</p>
      </div>
      <div className="relative">
        <svg
          viewBox="0 0 340 120"
          className="h-32 w-full"
          aria-hidden="true"
        >
          <path
            d={path}
            stroke="var(--border-subtle)"
            strokeWidth="1.5"
            fill="none"
            strokeDasharray="3 3"
          />
        </svg>
        <motion.div
          key={playKey}
          initial={{ offsetDistance: '0%' }}
          animate={{ offsetDistance: '100%' }}
          transition={{ duration: 1.4, ease: easings.md3Standard }}
          style={{
            offsetPath: `path('${path}')`,
            offsetRotate: '0deg',
          }}
          className="absolute top-0 left-0 h-6 w-6 rounded-full bg-[var(--text-primary)]"
        />
      </div>
    </div>
  );
}

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

  return (
    <div
      ref={rootRef}
      data-lab-island
      className="grid w-full gap-4 p-4 sm:grid-cols-2 lg:p-6"
    >
      {paths.map((p) => (
        <PathLane
          key={p.key}
          label={p.label}
          jp={p.jp}
          note={p.note}
          path={p.path}
          playKey={playKey}
        />
      ))}
    </div>
  );
}