GESTURE / 232
Pinch & rotate
二本の指で拡大・回転
二つの接点の距離と角度を、拡大率と回転角へ変換します。
見本の中を操作して変化を試してください。
ひとつ試すなら
二本指、または二つのスライダーで同じ変化を試せます。
こんなときに
写真、地図、制作ツールの操作。
仕組みと使い方
二本指、または二つのスライダーで同じ変化を試せます。
対応状況は使用する機能と端末により異なります。公式資料も参照してください。
表現名には通称や説明のための名称を含みます。デモと掲載コードはこの図鑑のオリジナルです。作者について
実装コードを見る
<!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}}.lab-scene{touch-action:none;display:grid;place-items:center}.touch-object{display:grid;place-items:center;width:100px;height:90px;background:#292929;color:white;font-size:32px}</style><div class="sample"><div class="lab-scene"><div class="touch-object">g*e</div></div><div class="lab-controls"><label>拡大率<output>1</output><input aria-label="拡大率" type="range" min="0.4" max="1.8" step="0.05" value="1" data-key="scale"></label><label>回転角<output>0</output><input aria-label="回転角" type="range" min="-180" max="180" step="1" value="0" data-key="rotation"></label></div><div class="lab-actions"><button data-play>停止</button><button data-reset>位置を戻す</button></div><p class="lab-status" role="status"></p></div><script>const lab=(function runDomLab(root,cfg,setup){
const abort=new AbortController(),values=Object.fromEntries(cfg.controls.map(c=>[c.key,c.value])),mq=matchMedia('(prefers-reduced-motion:reduce)');let auto=!mq.matches;
const on=(el,event,fn)=>el.addEventListener(event,fn,{signal:abort.signal});
const api=setup({root,values,kind:cfg.kind,on,reduced:mq.matches})||{};
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;api.update?.()});
const play=root.querySelector('[data-play]');on(play,'click',()=>{auto=!auto;api.setAuto?.(auto);for(const a of root.querySelector('.lab-scene').getAnimations({subtree:true}))auto?a.play():a.pause();play.textContent=auto?'停止':'再生';play.setAttribute('aria-pressed',String(!auto))});
const reset=root.querySelector('[data-reset]');on(reset,'click',()=>api.action?.());
on(document,'visibilitychange',()=>api.setAuto?.(auto&&!document.hidden));
api.update?.();if(cfg.static)play.hidden=true;
return {setAuto(on){auto=on&&!mq.matches;api.setAuto?.(auto);for(const a of root.querySelector('.lab-scene').getAnimations({subtree:true}))auto?a.play():a.pause();play.textContent=auto?'停止':'再生'},destroy(){abort.abort();api.destroy?.()}};
})(document,{"kind":"gesture-pinch","controls":[{"key":"scale","label":"拡大率","min":0.4,"max":1.8,"value":1,"step":0.05},{"key":"rotation","label":"回転角","min":-180,"max":180,"value":0,"step":1}],"static":true,"action":"位置を戻す"},function transformGesture({root,values:v,on}){const stage=root.querySelector('.lab-scene'),object=root.querySelector('.touch-object'),status=root.querySelector('.lab-status'),points=new Map();let initial=null,scale=1,angle=0;
function paint(){object.style.transform=`scale(${scale}) rotate(${angle}deg)`;status.textContent=`${scale.toFixed(2)}× / ${Math.round(angle)}°`;}
function measure(){const [a,b]=[...points.values()];return{distance:Math.hypot(a.x-b.x,a.y-b.y),angle:Math.atan2(b.y-a.y,b.x-a.x)*180/Math.PI}};
on(stage,'pointerdown',e=>{stage.setPointerCapture(e.pointerId);points.set(e.pointerId,{x:e.clientX,y:e.clientY});if(points.size===2)initial={...measure(),scale,rotation:angle};});
on(stage,'pointermove',e=>{if(!points.has(e.pointerId))return;points.set(e.pointerId,{x:e.clientX,y:e.clientY});if(points.size===2&&initial){const m=measure();scale=Math.max(.4,Math.min(1.8,initial.scale*m.distance/Math.max(1,initial.distance)));angle=initial.rotation+m.angle-initial.angle;paint()}});
for(const event of ['pointerup','pointercancel'])on(stage,event,e=>{points.delete(e.pointerId);initial=null});
return{update(){scale=v.scale;angle=v.rotation;paint()},action(){scale=1;angle=0;paint()},destroy(){points.clear()}};
});addEventListener('pagehide',()=>lab.destroy(),{once:true});</script></html>