ピンチイン/アウト(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/reactFramer MotionPointer 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+ で利用可能。それ以前は touchmovee.touches にフォールバックが必要

🤖 AIプロンプトテンプレート

Reactでピンチイン/アウト(ズーム)を実装してください。以下の要件を満たしてください。

- @use-gesture/react、Framer Motion、またはPointer Events APIのいずれかを使用すること
- 2本指のピンチでスケール(拡大・縮小)を変更できること
- スケールに最小値・最大値を設定し、境界を超えた場合に弾性反発(rubberband)またはハードクランプすること
- PCではマウスホイールでズーム操作できること
- ピンチの中心点を基準にズームすること(ミッドポイントズーム)
- iOS SafariでページズームとのEventの競合を避けるためtouch-action: noneを設定すること

⚠️ このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。