INTERACTION / 170

Spring return

離すとばねで戻る
つまみを離すと、振動しながら元の位置へ戻ります。

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

ラボで詳しく試す ↗

ひとつ試すなら

引っ張って、離してみる

こんなときに

ドラッグ操作に弾力を加える表現。

仕組みと使い方

ばねの力と減衰を数値で計算しています。矢印キーでも試せます。

ばねspringドラッグ弾力戻る
実装コードを見る
<!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 .spring-field{height:130px;width:100%;position:relative;border:1px dashed #bbb;border-radius:10px;background:radial-gradient(#bdbcb4 .7px,transparent 1px) 0 0/15px 15px}.sample .spring-origin{position:absolute;width:45px;height:45px;left:50%;top:50%;transform:translate(-50%,-50%);border:1px solid #b0aea3;border-radius:50%}.sample button.spring-handle{position:absolute;width:45px;height:45px;padding:0;left:50%;top:50%;transform:translate(-50%,-50%);border:1px solid #222;border-radius:50%;background:#222;color:white;font-size:22px;touch-action:none;user-select:none;cursor:grab}.sample button.spring-handle.dragging{cursor:grabbing}</style><div class="sample"><div class="adv-wrap"><div class="spring-field"><i class="spring-origin"></i><button type="button" class="spring-handle" aria-label="つかんで動かす。矢印キーでも操作できます">↔</button></div><small>つまんで、離す。 / 矢印キーでも操作</small></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 spring(root,options){const c=lifecycle(root,options),field=root.querySelector('.spring-field'),handle=root.querySelector('.spring-handle');let x=0,y=0,vx=0,vy=0,frame=0,last=0,drag=null;const paint=()=>handle.style.translate=`${x}px ${y}px`;const reset=()=>{cancelAnimationFrame(frame);frame=0;x=y=vx=vy=0;paint()};function tick(t){if(!c.live)return;const dt=Math.min(.032,(t-last)/1000||.016);last=t;vx+=(-120*x-14*vx)*dt;vy+=(-120*y-14*vy)*dt;x+=vx*dt;y+=vy*dt;paint();if(Math.abs(x)+Math.abs(y)+Math.abs(vx)+Math.abs(vy)<.3){reset();return}frame=requestAnimationFrame(tick)}function release(e){if(drag===null||e.pointerId!==drag)return;const id=drag;drag=null;try{if(handle.hasPointerCapture(id))handle.releasePointerCapture(id)}catch{}handle.classList.remove('dragging');if(c.reduced){reset();return}last=performance.now();frame=requestAnimationFrame(tick)}c.on(handle,'pointerdown',e=>{if(e.button!==0)return;cancelAnimationFrame(frame);frame=0;drag=e.pointerId;vx=vy=0;handle.setPointerCapture(e.pointerId);handle.classList.add('dragging')});c.on(handle,'pointermove',e=>{if(e.pointerId!==drag)return;const r=field.getBoundingClientRect();x=Math.max(-r.width/2+25,Math.min(r.width/2-25,e.clientX-r.left-r.width/2));y=Math.max(-r.height/2+25,Math.min(r.height/2-25,e.clientY-r.top-r.height/2));paint()});c.on(handle,'pointerup',release);c.on(handle,'pointercancel',release);c.on(handle,'lostpointercapture',e=>{if(drag===e.pointerId){drag=null;reset()}});c.on(handle,'keydown',e=>{const d={ArrowLeft:[-55,0],ArrowRight:[55,0],ArrowUp:[0,-35],ArrowDown:[0,35]}[e.key];if(!d)return;e.preventDefault();reset();[x,y]=d;paint();if(c.reduced){reset();return}last=performance.now();frame=requestAnimationFrame(tick)});c.cleanup(()=>{reset();if(drag!==null){try{handle.releasePointerCapture(drag)}catch{}}});c.onPause(reset);return c})(document,{reducedMotion:matchMedia('(prefers-reduced-motion:reduce)').matches});addEventListener('pagehide',()=>control.destroy(),{once:true});</script></html>