/* ============================================================ mobile-gestures.jsx Détecteur de gestes nommés pour smartphone. Chaque geste a un NOM SYSTÈME, et un composant de TEST. ============================================================ */ const { useState: uG, useRef: rG, useEffect: eG } = React; /* ============================================================ useGesture — hook bas niveau qui détecte les gestes Renvoie : { onTouchStart, onTouchMove, onTouchEnd } à attacher au composant qui doit recevoir les gestes. Callbacks supportés : onTap tap simple (< 200ms, ne bouge pas) onDoubleTap double-tap (deux tap rapides) onLongPress long press (≥ 500ms sans bouger) onSwipeLeft swipe vers la gauche onSwipeRight swipe vers la droite onSwipeUp swipe vers le haut onSwipeDown swipe vers le bas onPanStart début de glisser onPan cours de glisser ({dx, dy}) onPanEnd fin de glisser onPinch pincement ({scale, dx, dy}) ============================================================ */ function useGesture(handlers = {}) { const state = rG({ sx: 0, sy: 0, st: 0, lx: 0, ly: 0, lt: 0, moved: false, longPressTimer: null, lastTap: 0, lastTapPos: null, pinching: false, startDist: 0, }); const reset = () => { if (state.current.longPressTimer) clearTimeout(state.current.longPressTimer); }; const onTouchStart = (e) => { const t = e.touches[0]; state.current.sx = t.clientX; state.current.sy = t.clientY; state.current.lx = t.clientX; state.current.ly = t.clientY; state.current.st = Date.now(); state.current.lt = Date.now(); state.current.moved = false; // Pinch detection if (e.touches.length === 2) { const dx = e.touches[1].clientX - t.clientX; const dy = e.touches[1].clientY - t.clientY; state.current.startDist = Math.hypot(dx, dy); state.current.pinching = true; return; } // Long press if (handlers.onLongPress) { state.current.longPressTimer = setTimeout(() => { if (!state.current.moved) { handlers.onLongPress({ x: t.clientX, y: t.clientY }); state.current.moved = true; // empêche d'autres détections } }, 500); } handlers.onPanStart && handlers.onPanStart({ x: t.clientX, y: t.clientY }); }; const onTouchMove = (e) => { const t = e.touches[0]; const dx = t.clientX - state.current.sx; const dy = t.clientY - state.current.sy; if (Math.abs(dx) > 10 || Math.abs(dy) > 10) { state.current.moved = true; reset(); } if (state.current.pinching && e.touches.length === 2) { const px = e.touches[1].clientX - t.clientX; const py = e.touches[1].clientY - t.clientY; const dist = Math.hypot(px, py); const scale = dist / state.current.startDist; handlers.onPinch && handlers.onPinch({ scale, dx, dy }); return; } handlers.onPan && handlers.onPan({ dx, dy, x: t.clientX, y: t.clientY }); state.current.lx = t.clientX; state.current.ly = t.clientY; state.current.lt = Date.now(); }; const onTouchEnd = (e) => { reset(); const dx = state.current.lx - state.current.sx; const dy = state.current.ly - state.current.sy; const dt = Date.now() - state.current.st; handlers.onPanEnd && handlers.onPanEnd({ dx, dy }); if (state.current.pinching) { state.current.pinching = false; return; } if (state.current.moved && dt < 500) { const absX = Math.abs(dx), absY = Math.abs(dy); if (absX > 50 || absY > 50) { if (absX > absY) { if (dx > 0) handlers.onSwipeRight && handlers.onSwipeRight({ dx, dt }); else handlers.onSwipeLeft && handlers.onSwipeLeft({ dx, dt }); } else { if (dy > 0) handlers.onSwipeDown && handlers.onSwipeDown({ dy, dt }); else handlers.onSwipeUp && handlers.onSwipeUp({ dy, dt }); } } } else if (!state.current.moved && dt < 200) { // Tap / DoubleTap const now = Date.now(); const pos = { x: state.current.lx, y: state.current.ly }; const lp = state.current.lastTapPos; if (now - state.current.lastTap < 300 && lp && Math.hypot(pos.x - lp.x, pos.y - lp.y) < 30) { handlers.onDoubleTap && handlers.onDoubleTap(pos); state.current.lastTap = 0; } else { handlers.onTap && handlers.onTap(pos); state.current.lastTap = now; state.current.lastTapPos = pos; } } }; return { onTouchStart, onTouchMove, onTouchEnd }; } /* ============================================================ GestureZone — zone tactile de test Affiche le dernier geste détecté + un journal des gestes. Toutes les actions sont nommées explicitement. ============================================================ */ function GestureZone({ label, accept = [] }) { const [last, setLast] = uG(null); const [log, setLog] = uG([]); const [count, setCount] = uG({}); const [trail, setTrail] = uG(null); const fire = (name, data) => { setLast({ name, data, time: Date.now() }); setLog((l) => [{ name, t: new Date().toLocaleTimeString('fr-FR', { hour12: false }) }, ...l].slice(0, 5)); setCount((c) => ({ ...c, [name]: (c[name] || 0) + 1 })); }; const hAll = { onTap: () => fire('Tap'), onDoubleTap: () => fire('DoubleTap'), onLongPress: () => fire('LongPress'), onSwipeLeft: () => fire('SwipeLeft'), onSwipeRight: () => fire('SwipeRight'), onSwipeUp: () => fire('SwipeUp'), onSwipeDown: () => fire('SwipeDown'), onPan: ({ dx, dy }) => setTrail({ dx, dy }), onPanEnd: () => setTrail(null), onPinch: ({ scale }) => fire('Pinch', { scale: scale.toFixed(2) }), }; // Filtre uniquement les handlers demandés const h = accept.length === 0 ? hAll : Object.fromEntries( Object.entries(hAll).filter(([k]) => accept.some((n) => k.toLowerCase().includes(n.toLowerCase()))) ); const gesture = useGesture(h); return (