PHYSICS / 198
Gravity & restitution
重力と跳ね返り
加速度で落下させ、床に触れた瞬間に速度を反転・減衰させます。
見本の中を操作して変化を試してください。
ひとつ試すなら
球を持ち上げて離す。跳ね返り0と0.9の違いを見てください。
こんなときに
落ちる物体、ボールの運動。
仕組みと使い方
球を持ち上げて離す。跳ね返り0と0.9の違いを見てください。
対応状況は使用する機能と端末により異なります。公式資料も参照してください。
表現名には通称や説明のための名称を含みます。デモと掲載コードはこの図鑑のオリジナルです。作者について
実装コードを見る
<!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:330px}.sample{width:100%;height:100%;min-height:330px;padding:12px!important;display:flex!important;flex-direction:column;justify-content:center;align-items:center;gap:8px!important;background:#f0efeb;color:#232323;font-family:Arial,Meiryo,sans-serif;overflow:hidden;box-sizing:border-box}.sample *{box-sizing:border-box}.lab-scene{width:100%;max-width:430px;height:140px;position:relative;overflow:hidden;border-bottom:1px solid #ccc}.lab-scene:has(canvas){max-width:320px;height:auto;aspect-ratio:16/9;flex-shrink:0}.lab-canvas{display:block;width:100%;height:100%!important;outline-offset:-3px}.lab-canvas:focus-visible{outline:2px solid #222}.lab-controls{width:100%;max-width:390px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 14px}.lab-controls label{display:grid;grid-template-columns:1fr auto;gap:2px;font-size:11px;min-width:0}.lab-controls input{grid-column:1/-1;width:100%;min-width:0;margin:0;accent-color:#333;height:30px}.lab-controls output{font-variant-numeric:tabular-nums}.lab-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:center}.sample .lab-actions button{color:inherit;font:11px Arial,Meiryo,sans-serif;border:1px solid #aaa;border-radius:99px;background:transparent;min-height:44px;padding:8px 12px;cursor:pointer}.lab-status{font-size:10px;line-height:1.4;min-height:14px;margin:0;text-align:center}.lab-note{font-size:9px;line-height:1.3;color:#666;margin:0;text-align:center}.lab-actions button:focus-visible{outline:2px solid #222;outline-offset:2px}@media(prefers-reduced-motion:reduce){.sample *{animation-play-state:paused!important;transition:none!important}}</style><div class="sample"><div class="lab-scene"><canvas class="lab-canvas" tabindex="0" role="img" aria-label="重力と跳ね返り。矢印キーでも位置を変えられます。" style="touch-action:none"></canvas></div><div class="lab-controls"><label>重力<output>240</output><input aria-label="重力" type="range" min="30" max="600" step="1" value="240" data-key="gravity"></label><label>跳ね返り<output>0.7</output><input aria-label="跳ね返り" type="range" min="0" max="0.95" step="0.05" value="0.7" data-key="bounce"></label></div><div class="lab-actions"><button type="button" data-play>停止</button><button type="button" data-reset>最初から</button></div><p class="lab-status"></p></div><script>const lab=(function runCanvasLab(root,cfg,draw){
const canvas=root.querySelector('canvas'),ctx=canvas.getContext('2d'),status=root.querySelector('.lab-status'),play=root.querySelector('[data-play]'),reset=root.querySelector('[data-reset]'),abort=new AbortController(),mq=matchMedia('(prefers-reduced-motion:reduce)');
const values=Object.fromEntries(cfg.controls.map(c=>[c.key,c.value]));let state={},time=0,frame=0,last=0,allowed=!mq.matches,userPaused=false,destroyed=false,visible=true;
const pointer={x:160,y:90,down:false,active:false,dx:0,dy:0};let event='init';
const on=(el,type,fn,opts={})=>el.addEventListener(type,fn,{...opts,signal:abort.signal});
const circle=(x,y,r,fill='#292929')=>{ctx.beginPath();ctx.arc(x,y,Math.max(.1,r),0,Math.PI*2);ctx.fillStyle=fill;ctx.fill()};
const line=(x,y,a,b,color='#aaa',width=1)=>{ctx.beginPath();ctx.strokeStyle=color;ctx.lineWidth=width;ctx.moveTo(x,y);ctx.lineTo(a,b);ctx.stroke()};
const text=(str,x,y,size=12,color='#333',align='center')=>{ctx.fillStyle=color;ctx.font=`${size>25?'700':'400'} ${size}px Arial, sans-serif`;ctx.textAlign=align;ctx.fillText(str,x,y)};
function render(dt=0){if(destroyed||!ctx)return;ctx.setTransform(canvas.width/320,0,0,canvas.height/180,0,0);ctx.clearRect(0,0,320,180);ctx.save();draw({ctx,w:320,h:180,time,dt,values,state,pointer,event,kind:cfg.kind,circle,line,text,status:s=>{status.textContent=s}});ctx.restore();event='';}
function tick(now){frame=0;if(!running())return;const dt=Math.min((now-(last||now))/1000,.033);last=now;time+=dt;render(dt);frame=requestAnimationFrame(tick)}
function running(){return !destroyed&&allowed&&!userPaused&&visible&&!document.hidden&&cfg.animated!==false}
function sync(){cancelAnimationFrame(frame);frame=0;last=0;if(running())frame=requestAnimationFrame(tick);play.textContent=running()?'停止':'再生';play.setAttribute('aria-pressed',String(userPaused));}
function resize(){const dpr=Math.min(devicePixelRatio||1,2);canvas.width=Math.round(640*dpr);canvas.height=Math.round(360*dpr);render()}
on(play,'click',()=>{userPaused=running();if(!userPaused)allowed=true;sync();render()});
on(reset,'click',()=>{state={};time=0;event='reset';pointer.down=false;render();sync()});
for(const input of root.querySelectorAll('[data-key]')){on(input,'input',()=>{values[input.dataset.key]=Number(input.value);input.closest('label').querySelector('output').textContent=input.value;event='input';render()})}
function position(e){const b=canvas.getBoundingClientRect(),x=(e.clientX-b.left)/b.width*320,y=(e.clientY-b.top)/b.height*180;pointer.dx=x-pointer.x;pointer.dy=y-pointer.y;pointer.x=Math.max(0,Math.min(320,x));pointer.y=Math.max(0,Math.min(180,y));pointer.active=true;}
on(canvas,'pointerdown',e=>{position(e);pointer.down=true;event='down';canvas.setPointerCapture(e.pointerId);render()});
on(canvas,'pointermove',e=>{position(e);event='move';render()});
on(canvas,'pointerup',e=>{position(e);pointer.down=false;event='up';render()});
on(canvas,'pointercancel',()=>{pointer.down=false;event='cancel';render()});
on(canvas,'pointerleave',()=>{pointer.active=false;if(!pointer.down){event='leave';render()}});
on(canvas,'keydown',e=>{const keys={ArrowLeft:[-12,0],ArrowRight:[12,0],ArrowUp:[0,-12],ArrowDown:[0,12]};if(keys[e.key]){e.preventDefault();pointer.active=true;pointer.x=Math.max(0,Math.min(320,pointer.x+keys[e.key][0]));pointer.y=Math.max(0,Math.min(180,pointer.y+keys[e.key][1]));event='keyboard';render()}if(e.key===' '||e.key==='Enter'){e.preventDefault();pointer.down=!pointer.down;pointer.active=true;event=pointer.down?'down':'up';render()}});
on(document,'visibilitychange',sync);on(mq,'change',()=>{allowed=!mq.matches;sync();render()});
const observer=new IntersectionObserver(entries=>{visible=entries[0].isIntersecting;sync()});observer.observe(canvas);resize();sync();
if(cfg.animated===false)play.hidden=true;
return {setAuto(on){allowed=on&&!mq.matches;sync()},destroy(){destroyed=true;cancelAnimationFrame(frame);abort.abort();observer.disconnect();state={}}};
})(document,{"kind":"physics-gravity","controls":[{"key":"gravity","label":"重力","min":30,"max":600,"value":240,"step":1},{"key":"bounce","label":"跳ね返り","min":0,"max":0.95,"value":0.7,"step":0.05}],"drag":true},function physics({ctx,time,dt,values:v,state:s,pointer:p,event,kind,circle,line,text,status}){
if(!s.ready){Object.assign(s,{ready:true,x:85,y:45,vx:100,vy:0,angle:.8,omega:0});}
const k=kind.split('-')[1];
if(k==='spring'){
const target=p.down||event==='keyboard'?p.x:160;if(p.down||event==='keyboard'){s.x=p.x;s.vx=0}else{const a=(target-s.x)*v.stiffness-s.vx*v.damping;s.vx+=a*dt;s.x+=s.vx*dt;s.x=Math.max(18,Math.min(302,s.x))}
line(35,90,285,90,'#ccc');line(160,60,160,120,'#bbb');circle(s.x,90,17);text('spring',70,150);const progress=(time%2)/2;circle(35+250*(1-Math.pow(1-progress,3)),34,5,'#999');text('ease-out',280,39,9);status('球をつかんで離す · Spaceでつかむ/離す');
}else if(k==='gravity'){
if(event==='down'||event==='keyboard'){s.x=p.x;s.y=Math.min(p.y,140);s.vy=0}
if(!p.down){s.vy+=v.gravity*dt;s.y+=s.vy*dt;if(s.y>146){s.y=146;s.vy=-Math.abs(s.vy)*v.bounce}}else{s.x=p.x;s.y=Math.min(p.y,146)}
line(15,162,305,162);circle(s.x,s.y,16);text('gravity / restitution',160,22,10);status('高さと跳ね返りを比べる · タップで落とす');
}else if(k==='pendulum'){
if(p.down||event==='keyboard'){s.angle=Math.atan2(p.x-160,Math.max(5,p.y-12));s.omega=0}else{s.omega+=(-v.gravity/100*Math.sin(s.angle)-v.damping*s.omega)*dt;s.angle+=s.omega*dt}
const x=160+125*Math.sin(s.angle),y=20+125*Math.cos(s.angle);line(160,20,x,y,'#555',2);circle(160,20,4);circle(x,y,15);status('振れ幅を変えて、戻る速さを見る');
}else{
if(event==='down'||event==='keyboard'){s.x=p.x;s.y=p.y;s.vx=0;s.vy=0}
if(p.down){s.vx=p.dx*25;s.vy=p.dy*25;s.x=p.x;s.y=p.y}else{s.vx*=Math.exp(-v.friction*dt);s.vy*=Math.exp(-v.friction*dt);s.x+=s.vx*dt;s.y+=s.vy*dt;for(const [pos,vel,min,max] of [['x','vx',15,305],['y','vy',15,160]]){if(s[pos]<min||s[pos]>max){s[pos]=Math.max(min,Math.min(max,s[pos]));s[vel]*=-v.bounce}}}
ctx.strokeStyle='#ccc';ctx.strokeRect(1,1,318,173);circle(s.x,s.y,14);status('つかんで投げる · 摩擦0なら速度が保たれる');
}
});addEventListener('pagehide',()=>lab.destroy(),{once:true});</script></html>