rigid 600ms · elastic 400ms · gas 1000ms
Material Easing
モーションを素材感(金属・ゴム・水・紙・煙・ガラス)で捉える。同じ移動でも質感パラメータで体感がまったく変わる。
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>
);
} @keyframes material-move {
0% { transform: translateX(0) scale(1); }
75% { transform: translateX(var(--travel)) scale(var(--overshoot, 1)); }
100% { transform: translateX(var(--travel)) scale(1); }
}
.material-block { animation: material-move var(--duration) var(--ease) forwards; }
.material-rigid { --duration: 600ms; --ease: cubic-bezier(0.4, 0, 0.6, 1); }
.material-elastic { --duration: 400ms; --ease: cubic-bezier(0.175, 0.885, 0.32, 1.275); --overshoot: 1.2; }
.material-fluid { --duration: 750ms; --ease: cubic-bezier(0.5, 0, 0.5, 1); --overshoot: 1.05; }
.material-paper { --duration: 500ms; --ease: cubic-bezier(0.2, 0, 0, 1); --overshoot: 1.04; }
.material-gas { --duration: 1000ms; --ease: cubic-bezier(0.4, 0, 0.6, 1); }
.material-glass { --duration: 450ms; --ease: cubic-bezier(0.7, 0, 0.84, 0); } Build 6 material-driven motion lanes (rigid / elastic / fluid / paper / gas / glass):
- base duration 0.5s x durationScale: rigid 600ms, elastic 400ms, fluid 750ms, paper 500ms, gas 1000ms, glass 450ms
- eases: rigid cubic-bezier(0.4, 0, 0.6, 1), elastic cubic-bezier(0.175, 0.885, 0.32, 1.275), fluid cubic-bezier(0.5, 0, 0.5, 1), paper cubic-bezier(0.2, 0, 0, 1), gas cubic-bezier(0.4, 0, 0.6, 1), glass cubic-bezier(0.7, 0, 0.84, 0)
- overshoot: elastic 0.2, fluid 0.05, paper 0.04, others 0 — animate x [0, travel, travel] + scale [1, 1+overshoot, 1] with times [0, 0.75, 1]
- gas instead scales to 1.4 with opacity 0.6 and slight blur (form dissipates)
- swatch colors identify each material; replay restarts all lanes via a shared key