ピンチイン/アウト(2点間距離・スケール補正)
2点間距離の計算精度・スケール補正の実装差を主要ライブラリで比較
💻 PC でご覧の方へ:@use-gesture はマウスホイールで操作可。Framer / Pointer Events も本ページではホイール対応済み。タッチ特有のノイズ・EMA 効果はスマートフォンで確認できます。
@use-gesture/react
読み込み中...
tsx
'use client';
import { usePinch } from '@use-gesture/react';
import { useSpring, animated } from '@react-spring/web';
export function UseGesturePinchDemo() {
const [{ scale }, api] = useSpring(() => ({ scale: 1 }));
const bind = usePinch(
({ offset: [s] }) => {
api.start({ scale: Math.max(0.5, Math.min(s, 3)) });
},
{ scaleBounds: { min: 0.5, max: 3 } }
);
return (
<div
className="flex items-center justify-center h-64 bg-gray-100 rounded-lg select-none overflow-hidden"
style={{ touchAction: 'none' }}
>
<animated.div
{...bind()}
style={{ scale }}
className="w-28 h-28 bg-violet-600 rounded-2xl flex items-center justify-center text-white font-bold text-lg"
>
ピンチ
</animated.div>
</div>
);
}実装差比較表
| 項目 | @use-gesture/react | Framer Motion | Pointer Events API |
|---|---|---|---|
| 距離計算 | EMA フィルタ済み da[0](ノイズ抑制) | 生 Euclidean √(Δx²+Δy²)(補正なし) | 生 Euclidean √(Δx²+Δy²)(補正なし) |
| スケール補正 | rubberband:境界超過後に弾性反発→自動復帰 | Spring 収束:境界でクランプ後にアニメーション停止 | ハードクランプのみ:境界で即座停止・弾性なし |
| スケール取得 | offset[0](開始時を 1 として正規化・累積) | currentDist / startDist × startScale(手動計算) | currentDist / startDist × startScale(手動計算) |
| ミッドポイント | origin プロパティで自動提供 | 手動計算:(x1+x2)/2, (y1+y2)/2 | 手動計算:(x1+x2)/2, (y1+y2)/2 |
| 速度・慣性 | velocity[0] として EMA 平滑化スケール速度を提供 | Spring の damping 係数で自然減衰 | 速度概念なし(実装が必要) |
| PC ホイール対応 | デフォルトで wheel イベントをピンチとして処理 | useEffect で手動追加(passive:false 必須) | useEffect で手動追加(passive:false 必須) |
| 実装コスト | usePinch 1 フック(設定のみ) | useMotionValue + useSpring + PointerEvent 全手実装 | PointerEvent 全手実装(最もコード量多) |
2点間距離の計算精度
@use-gesture — EMA(指数移動平均)フィルタ
内部で da[0] に EMA を適用し、タッチノイズ(指のブレ数px)を平滑化。スケール速度も同様に補正されるため、フリックリリース後の慣性量が安定する。
Framer Motion — 生 Euclidean + Spring
距離計算は √(Δx²+Δy²) の素の値。ただし useSpring が値の追従を物理的に遅延させるため、ノイズが高周波として現れても視覚的には目立ちにくい。精度より滑らかさを Spring に委ねるアプローチ。
Pointer Events API — 生 Euclidean のみ
補正機構が皆無なため、タッチのジター(±2〜3px の微小振動)がそのままスケール値に反映される。高解像度デバイスほど影響が出やすく、独自の LPF(ローパスフィルタ)実装が必要になる。
⚠️ iOS Safari での注意点
- • ピンチ対象要素に
touch-action: noneが必須(ページズームとの競合回避) - •
@use-gestureは内部で自動設定。Framer / Vanilla は手動でstyle={{ touchAction: "none" }}を指定 - • ホイールイベントのリスナーは
{ passive: false }で登録しないとpreventDefault()が無効になりページスクロールが発生する - • iOS 16+ の Safari はピンチでページ全体をズームしようとするため、
<meta name="viewport" content="user-scalable=no">の併用が有効(アクセシビリティとのトレードオフに注意) - •
setPointerCaptureは iOS Safari 13+ で利用可能。それ以前はtouchmove+e.touchesにフォールバックが必要
🤖 AIプロンプトテンプレート
Reactでピンチイン/アウト(ズーム)を実装してください。以下の要件を満たしてください。 - @use-gesture/react、Framer Motion、またはPointer Events APIのいずれかを使用すること - 2本指のピンチでスケール(拡大・縮小)を変更できること - スケールに最小値・最大値を設定し、境界を超えた場合に弾性反発(rubberband)またはハードクランプすること - PCではマウスホイールでズーム操作できること - ピンチの中心点を基準にズームすること(ミッドポイントズーム) - iOS SafariでページズームとのEventの競合を避けるためtouch-action: noneを設定すること
⚠️ このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。