linear · arc · s-curve · spiral · 1.4s
Path Types
同じ A → B の移動でも経路(直線・アーク・S 字・スパイラル)で体感がまったく変わることを CSS Motion Path で比較する。
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>
);
} .path-dot {
offset-rotate: 0deg;
animation: path-travel 1.4s cubic-bezier(0.2, 0, 0, 1) forwards;
}
@keyframes path-travel {
from { offset-distance: 0%; }
to { offset-distance: 100%; }
}
.path-line { offset-path: path('M 20 60 L 320 60'); }
.path-arc { offset-path: path('M 20 60 Q 170 -10 320 60'); }
.path-scurve { offset-path: path('M 20 60 C 100 0, 240 120, 320 60'); }
.path-spiral { offset-path: path('M 320 60 C 280 -40, 80 120, 60 40 S 240 100, 320 60'); } Build 4 path-type comparisons (Linear / Arc / S-curve / Spiral):
- SVG paths (340x120): line 'M 20 60 L 320 60', arc 'M 20 60 Q 170 -10 320 60', s-curve 'M 20 60 C 100 0, 240 120, 320 60', spiral 'M 320 60 C 280 -40, 80 120, 60 40 S 240 100, 320 60'
- draw a dashed muted guide path + an animated dot traveling via CSS motion path (offset-path + offset-distance 0% -> 100%)
- duration 1.4s, easing cubic-bezier(0.2, 0, 0, 1)
- label each with the emotional quality (purposeful / joyful / elegant / whimsical)
- replay restarts all four dots simultaneously via a shared key