Changes
This commit is contained in:
@@ -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,19 +29,29 @@ 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 handleTouchStart = (e: TouchEvent) => {
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
swipeStateRef.current = {
|
swipeStateRef.current = {
|
||||||
startX: touch.clientX,
|
startX: touch.clientX,
|
||||||
startY: touch.clientY,
|
startY: touch.clientY,
|
||||||
startTime: Date.now(),
|
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;
|
||||||
@@ -56,7 +67,7 @@ export const useSwipeControls = ({
|
|||||||
|
|
||||||
// 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;
|
swipeStateRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -71,35 +82,29 @@ export const useSwipeControls = ({
|
|||||||
direction = deltaY > 0 ? 'down' : 'up';
|
direction = deltaY > 0 ? 'down' : 'up';
|
||||||
}
|
}
|
||||||
|
|
||||||
onSwipe(direction, isFastSwipe);
|
onSwipeRef.current(direction, isFastSwipe);
|
||||||
swipeStateRef.current = null;
|
swipeStateRef.current = null;
|
||||||
}, [enabled, minSwipeDistance, fastSwipeThreshold, onSwipe, onTap]);
|
};
|
||||||
|
|
||||||
const handleTouchCancel = useCallback(() => {
|
const handleTouchCancel = () => {
|
||||||
swipeStateRef.current = null;
|
swipeStateRef.current = null;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
// Returns handlers to attach to an element
|
// Attach to document for full-screen gesture detection
|
||||||
const getSwipeHandlers = useCallback(() => ({
|
document.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||||
onTouchStart: (e: React.TouchEvent) => handleTouchStart(e.nativeEvent),
|
document.addEventListener('touchend', handleTouchEnd, { passive: true });
|
||||||
onTouchEnd: (e: React.TouchEvent) => handleTouchEnd(e.nativeEvent),
|
document.addEventListener('touchcancel', handleTouchCancel, { passive: true });
|
||||||
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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user