This commit is contained in:
gpt-engineer-app[bot]
2026-01-16 18:15:42 +00:00
parent 623e88e2c7
commit 4c09953d9b
2 changed files with 80 additions and 79 deletions
+63 -58
View File
@@ -19,6 +19,7 @@ interface UseSwipeControlsOptions {
/** /**
* Hook for detecting swipe gestures on touch devices * Hook for detecting swipe gestures on touch devices
* Supports directional swipes and fast swipe detection (for hard drop in Tetris) * Supports directional swipes and fast swipe detection (for hard drop in Tetris)
* Attaches to document for full-screen touch detection
*/ */
export const useSwipeControls = ({ export const useSwipeControls = ({
onSwipe, onSwipe,
@@ -28,78 +29,82 @@ export const useSwipeControls = ({
enabled = true, enabled = true,
}: UseSwipeControlsOptions) => { }: UseSwipeControlsOptions) => {
const swipeStateRef = useRef<SwipeState | null>(null); const swipeStateRef = useRef<SwipeState | null>(null);
const onSwipeRef = useRef(onSwipe);
const onTapRef = useRef(onTap);
const handleTouchStart = useCallback((e: TouchEvent) => { // Keep refs updated
useEffect(() => {
onSwipeRef.current = onSwipe;
onTapRef.current = onTap;
}, [onSwipe, onTap]);
useEffect(() => {
if (!enabled) return; if (!enabled) return;
const touch = e.touches[0];
swipeStateRef.current = { const handleTouchStart = (e: TouchEvent) => {
startX: touch.clientX, const touch = e.touches[0];
startY: touch.clientY, swipeStateRef.current = {
startTime: Date.now(), startX: touch.clientX,
startY: touch.clientY,
startTime: Date.now(),
};
}; };
}, [enabled]);
const handleTouchEnd = useCallback((e: TouchEvent) => { const handleTouchEnd = (e: TouchEvent) => {
if (!enabled || !swipeStateRef.current) return; if (!swipeStateRef.current) return;
const touch = e.changedTouches[0]; const touch = e.changedTouches[0];
const { startX, startY, startTime } = swipeStateRef.current; const { startX, startY, startTime } = swipeStateRef.current;
const deltaX = touch.clientX - startX; const deltaX = touch.clientX - startX;
const deltaY = touch.clientY - startY; const deltaY = touch.clientY - startY;
const deltaTime = Date.now() - startTime; const deltaTime = Date.now() - startTime;
const absX = Math.abs(deltaX); const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY); const absY = Math.abs(deltaY);
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const speed = distance / Math.max(deltaTime, 1); const speed = distance / Math.max(deltaTime, 1);
// If movement is too small, treat as tap // If movement is too small, treat as tap
if (distance < minSwipeDistance) { if (distance < minSwipeDistance) {
onTap?.(); onTapRef.current?.();
swipeStateRef.current = null;
return;
}
const isFastSwipe = speed > fastSwipeThreshold;
// Determine direction based on larger axis
let direction: SwipeDirection;
if (absX > absY) {
direction = deltaX > 0 ? 'right' : 'left';
} else {
direction = deltaY > 0 ? 'down' : 'up';
}
onSwipeRef.current(direction, isFastSwipe);
swipeStateRef.current = null; swipeStateRef.current = null;
return; };
}
const isFastSwipe = speed > fastSwipeThreshold; const handleTouchCancel = () => {
swipeStateRef.current = null;
};
// Determine direction based on larger axis // Attach to document for full-screen gesture detection
let direction: SwipeDirection; document.addEventListener('touchstart', handleTouchStart, { passive: true });
if (absX > absY) { document.addEventListener('touchend', handleTouchEnd, { passive: true });
direction = deltaX > 0 ? 'right' : 'left'; document.addEventListener('touchcancel', handleTouchCancel, { passive: true });
} else {
direction = deltaY > 0 ? 'down' : 'up';
}
onSwipe(direction, isFastSwipe);
swipeStateRef.current = null;
}, [enabled, minSwipeDistance, fastSwipeThreshold, onSwipe, onTap]);
const handleTouchCancel = useCallback(() => {
swipeStateRef.current = null;
}, []);
// Returns handlers to attach to an element
const getSwipeHandlers = useCallback(() => ({
onTouchStart: (e: React.TouchEvent) => handleTouchStart(e.nativeEvent),
onTouchEnd: (e: React.TouchEvent) => handleTouchEnd(e.nativeEvent),
onTouchCancel: () => handleTouchCancel(),
}), [handleTouchStart, handleTouchEnd, handleTouchCancel]);
// Alternative: attach to a specific element ref
const bindToElement = useCallback((element: HTMLElement | null) => {
if (!element) return;
element.addEventListener('touchstart', handleTouchStart, { passive: true });
element.addEventListener('touchend', handleTouchEnd, { passive: true });
element.addEventListener('touchcancel', handleTouchCancel, { passive: true });
return () => { return () => {
element.removeEventListener('touchstart', handleTouchStart); document.removeEventListener('touchstart', handleTouchStart);
element.removeEventListener('touchend', handleTouchEnd); document.removeEventListener('touchend', handleTouchEnd);
element.removeEventListener('touchcancel', handleTouchCancel); document.removeEventListener('touchcancel', handleTouchCancel);
}; };
}, [handleTouchStart, handleTouchEnd, handleTouchCancel]); }, [enabled, minSwipeDistance, fastSwipeThreshold]);
// Legacy handlers for specific element binding (not used with document-level)
const getSwipeHandlers = useCallback(() => ({}), []);
const bindToElement = useCallback(() => () => {}, []);
return { getSwipeHandlers, bindToElement }; return { getSwipeHandlers, bindToElement };
}; };
+7 -11
View File
@@ -20,7 +20,7 @@ const games = [
id: 'pacman', id: 'pacman',
name: 'Pac-Man', name: 'Pac-Man',
description: 'Navigate the maze, eat dots, avoid ghosts', description: 'Navigate the maze, eat dots, avoid ghosts',
hasMultiplayer: false, hasMultiplayer: true,
ascii: `┌────────┐ ascii: `┌────────┐
│· · ᗣ · │ │· · ᗣ · │
│ ┌─┐ ┌─┐│ │ ┌─┐ ┌─┐│
@@ -44,7 +44,7 @@ const games = [
id: 'breakout', id: 'breakout',
name: 'Breakout', name: 'Breakout',
description: 'Break bricks with a bouncing ball', description: 'Break bricks with a bouncing ball',
hasMultiplayer: false, hasMultiplayer: true,
ascii: `┌────────┐ ascii: `┌────────┐
│████████│ │████████│
│▓▓▓▓▓▓▓▓│ │▓▓▓▓▓▓▓▓│
@@ -110,9 +110,9 @@ const Games = () => {
</p> </p>
</div> </div>
{/* Multiplayer Badge */} {/* Multiplayer Badge - using primary colors instead of purple */}
{game.hasMultiplayer && ( {game.hasMultiplayer && (
<span className="absolute top-3 right-3 font-pixel text-[10px] text-purple-400 border border-purple-500/60 bg-purple-500/10 px-2 py-1 rounded-sm"> <span className="absolute top-3 right-3 font-pixel text-[10px] text-primary border border-primary/60 bg-primary/10 px-2 py-1 rounded-sm">
2P 2P
</span> </span>
)} )}
@@ -123,13 +123,9 @@ const Games = () => {
</div> </div>
{/* Footer */} {/* Footer */}
<div className="border border-primary/30 p-3 bg-background/30 mt-6 flex items-center justify-between"> <div className="mt-auto pt-6">
<p className="font-pixel text-xs text-foreground/50"> <p className="font-pixel text-xs text-foreground/40 text-center">
<span className="text-primary">{'>'}</span> Max score: 4,294,967,296 Games auto-save high scores 2P games require keyboard
<span className="text-foreground/30 ml-2">(uint32 overflow)</span>
</p>
<p className="font-pixel text-xs text-foreground/40 hidden md:block">
Arrow keys / WASD to control
</p> </p>
</div> </div>
</motion.div> </motion.div>