MOTION / 172

Eased motion path

曲線に沿って加減速
曲線上の位置を求め、加速と減速を加えて移動させます。

見本の中を操作して変化を試してください。

ラボで詳しく試す ↗

ひとつ試すなら

どこが変わるかを観察して、似た表現と見比べてみよう。

こんなときに

軌道や経路をたどる演出。

仕組みと使い方

この例はSVGのパスから座標を取り出しています。CSSのoffset-pathでも経路を指定できます。

motion path曲線軌道orbit加減速
実装コードを見る
<!doctype html><html lang="ja"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{margin:0;min-height:300px}.sample{width:100%;height:100%;min-height:190px;position:relative;box-sizing:border-box;display:grid;place-items:center;overflow:hidden;padding:18px;background:#f4f3ee;color:#242424;font-family:Arial,Meiryo,sans-serif}.sample *{box-sizing:border-box}.sample button{font:600 11px Arial,Meiryo,sans-serif;border:1px solid #333;border-radius:999px;min-height:36px;padding:8px 14px;background:#fff;color:#222;cursor:pointer;touch-action:manipulation}.sample :focus-visible{outline:2px solid #555;outline-offset:4px}.sample small{font-size:10px;color:#666;line-height:1.5}.sample .adv-wrap{width:min(100%,260px);display:flex;flex-direction:column;align-items:center;gap:10px}.sample .adv-status{min-height:13px;text-align:center;font-size:10px;color:#666}.sample svg{max-width:100%}@media(prefers-reduced-motion:reduce){.sample *{transition:none!important;animation:none!important}}.sample .adv-wrap{gap:7px}</style><div class="sample"><div class="adv-wrap"><svg viewBox="0 0 260 120" width="260" height="105" role="img" aria-label="曲線を一周する点"><path class="orbit-path" d="M35 60C35 0 225 0 225 60C225 120 35 120 35 60" fill="none" stroke="#a9a79d" stroke-width="1.5" stroke-dasharray="3 4"/><circle class="orbit-dot" cx="35" cy="60" r="12" fill="#222"/><text x="130" y="64" text-anchor="middle" fill="#777" font-size="10" font-family="monospace">EASE IN / OUT</text></svg><button type="button" class="orbit-play">一周させる ↗</button><span class="adv-status">0%</span></div></div><script>const lifecycle=function lifecycle(root,options={}){const abort=new AbortController(),animations=new Set(),cleanup=[],pause=[];const os=globalThis.matchMedia?.('(prefers-reduced-motion:reduce)').matches||false;let reduced=options.reducedMotion??os,live=true;return{get reduced(){return reduced},get live(){return live},on(node,event,fn,opts={}){node.addEventListener(event,fn,{...opts,signal:abort.signal})},track(a){animations.add(a);a.finished.then(()=>animations.delete(a),()=>animations.delete(a));return a},stop(){animations.forEach(a=>a.cancel());animations.clear()},cleanup(fn){cleanup.push(fn)},onPause(fn){pause.push(fn)},destroy(){if(!live)return;live=false;abort.abort();this.stop();cleanup.forEach(fn=>fn())},setAuto(value){reduced=!value||os;if(reduced){this.stop();pause.forEach(fn=>fn())}}}};const control=(function orbit(root,options){const c=lifecycle(root,options),path=root.querySelector('.orbit-path'),dot=root.querySelector('.orbit-dot'),button=root.querySelector('.orbit-play'),label=root.querySelector('.adv-status');let frame=0;const length=path.getTotalLength();const place=p=>{const q=path.getPointAtLength(length*p);dot.setAttribute('cx',q.x);dot.setAttribute('cy',q.y);label.textContent=Math.round(p*100)+'%'};const stop=()=>{cancelAnimationFrame(frame);frame=0};c.on(button,'click',()=>{stop();if(c.reduced){place(1);return}const start=performance.now();const tick=t=>{if(!c.live)return;const p=Math.min(1,(t-start)/2200),ease=p<.5?4*p*p*p:1-Math.pow(-2*p+2,3)/2;place(ease);if(p<1)frame=requestAnimationFrame(tick);else frame=0};place(0);frame=requestAnimationFrame(tick)});c.cleanup(stop);c.onPause(()=>{stop();place(1)});place(0);return c})(document,{reducedMotion:matchMedia('(prefers-reduced-motion:reduce)').matches});addEventListener('pagehide',()=>control.destroy(),{once:true});</script></html>