super-mario / app /components /GameCanvas.tsx
asemxin
feat: reduce mobile control sensitivity for better touch experience
0c4f8d5
'use client';
import { useEffect, useRef, useCallback, useState } from 'react';
import { Game } from '@/lib/engine/Game';
import { GAME_CONFIG } from '@/lib/constants';
import TouchControls from './TouchControls';
interface GameCanvasProps {
onGameOver: (isWin: boolean, score: number) => void;
startLevel?: number;
}
interface Dimensions {
width: number;
height: number;
}
export default function GameCanvas({ onGameOver, startLevel = 0 }: GameCanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const gameRef = useRef<Game | null>(null);
const [isMobile, setIsMobile] = useState(false);
const [dimensions, setDimensions] = useState<Dimensions>({
width: GAME_CONFIG.CANVAS_WIDTH,
height: GAME_CONFIG.CANVAS_HEIGHT
});
const handleGameOver = useCallback((isWin: boolean, score: number) => {
onGameOver(isWin, score);
}, [onGameOver]);
const handleControlChange = useCallback((controls: { left: boolean; right: boolean; jump: boolean }) => {
if (gameRef.current) {
gameRef.current.setControls(controls);
}
}, []);
// Detect mobile and handle resize
useEffect(() => {
const checkMobile = () => {
const mobile = window.matchMedia('(max-width: 1024px)').matches ||
'ontouchstart' in window ||
navigator.maxTouchPoints > 0;
setIsMobile(mobile);
};
const handleResize = () => {
checkMobile();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const aspectRatio = GAME_CONFIG.CANVAS_WIDTH / GAME_CONFIG.CANVAS_HEIGHT;
// Reserve space for touch controls on mobile
const controlsHeight = isMobile ? 140 : 0;
const availableHeight = viewportHeight - controlsHeight - 20;
const availableWidth = viewportWidth - 20;
let newWidth, newHeight;
if (availableWidth / availableHeight > aspectRatio) {
// Height is limiting factor
newHeight = Math.min(availableHeight, GAME_CONFIG.CANVAS_HEIGHT);
newWidth = newHeight * aspectRatio;
} else {
// Width is limiting factor
newWidth = Math.min(availableWidth, GAME_CONFIG.CANVAS_WIDTH);
newHeight = newWidth / aspectRatio;
}
setDimensions({
width: Math.floor(newWidth),
height: Math.floor(newHeight)
});
};
handleResize();
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
};
}, [isMobile]);
useEffect(() => {
if (!canvasRef.current) return;
const game = new Game(canvasRef.current, startLevel, isMobile);
gameRef.current = game;
game.setOnGameOver(handleGameOver);
game.start();
return () => {
game.stop();
};
}, [handleGameOver, startLevel, isMobile]);
// Calculate scale for CSS transform
const scale = dimensions.width / GAME_CONFIG.CANVAS_WIDTH;
return (
<div className="game-wrapper" ref={containerRef}>
<div
className="canvas-container"
style={{
width: dimensions.width,
height: dimensions.height,
}}
>
<canvas
ref={canvasRef}
width={GAME_CONFIG.CANVAS_WIDTH}
height={GAME_CONFIG.CANVAS_HEIGHT}
className="game-canvas"
style={{
width: dimensions.width,
height: dimensions.height,
}}
tabIndex={0}
/>
</div>
{isMobile && <TouchControls onControlChange={handleControlChange} />}
</div>
);
}