Improve canvas zooming and scrolling with mouse wheel for smoother interaction

Implement mouse wheel zooming and smooth scrolling for the canvas component, enhance WebSocket connection management with automatic reconnection and keep-alive pings, and refine CSS for pixel hover effects.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 0385ea33-cde8-4bbd-8fce-8d192d30eb41
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/870d08ce-da3b-4822-9874-c2fe2b7628b1/0385ea33-cde8-4bbd-8fce-8d192d30eb41/PVrRiEe
This commit is contained in:
freesemar93
2025-08-18 12:40:00 +00:00
parent 93ddcaf807
commit 83fc03b313
8 changed files with 135 additions and 45 deletions

View File

@@ -57,6 +57,21 @@ export function Canvas({
setZoom(1);
};
const handleWheel = (e: React.WheelEvent) => {
e.preventDefault();
const zoomFactor = 1.1;
const delta = e.deltaY;
if (delta < 0) {
// Rein zoomen
setZoom(prev => Math.min(prev * zoomFactor, 3));
} else {
// Raus zoomen
setZoom(prev => Math.max(prev / zoomFactor, 0.5));
}
};
useEffect(() => {
setPixelSize(Math.max(2, 8 * zoom));
}, [zoom]);
@@ -71,11 +86,12 @@ export function Canvas({
<div className="flex-1 relative bg-canvas-bg overflow-hidden">
<div
ref={containerRef}
className="w-full h-full overflow-auto p-8"
className="w-full h-full overflow-auto p-8 scroll-smooth canvas-container"
onWheel={handleWheel}
data-testid="canvas-container"
>
{/* Coordinate System Container */}
<div className="relative inline-block">
<div className="relative inline-block canvas-zoom">
{/* Top X-axis coordinates */}
<div className="flex ml-8 mb-1">
{Array.from({ length: Math.ceil(canvasWidth / 10) }, (_, i) => (
@@ -124,7 +140,7 @@ export function Canvas({
<div
key={`${x}-${y}`}
className={cn(
"pixel cursor-pointer transition-all duration-100 hover:scale-110 hover:z-10 absolute",
"pixel cursor-pointer hover:scale-110 hover:z-10 absolute",
cooldownActive && "cursor-not-allowed"
)}
style={{

View File

@@ -5,40 +5,69 @@ export function useWebSocket(onMessage: (message: WSMessage) => void) {
const [isConnected, setIsConnected] = useState(false);
const [userCount, setUserCount] = useState(0);
const ws = useRef<WebSocket | null>(null);
const reconnectTimer = useRef<NodeJS.Timeout | null>(null);
const reconnectAttempts = useRef(0);
const connect = () => {
try {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws`;
console.log(`Connecting to WebSocket: ${wsUrl}`);
ws.current = new WebSocket(wsUrl);
ws.current.onopen = () => {
console.log("WebSocket connected");
setIsConnected(true);
reconnectAttempts.current = 0;
};
ws.current.onmessage = (event) => {
try {
const message: WSMessage = JSON.parse(event.data);
if (message.type === "user_count") {
setUserCount(message.data.count);
}
onMessage(message);
} catch (error) {
console.error("Failed to parse WebSocket message:", error);
}
};
ws.current.onclose = (event) => {
console.log("WebSocket disconnected:", event.code, event.reason);
setIsConnected(false);
// Automatically reconnect after a delay
if (reconnectAttempts.current < 5) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000);
console.log(`Reconnecting in ${delay}ms...`);
reconnectTimer.current = setTimeout(() => {
reconnectAttempts.current++;
connect();
}, delay);
}
};
ws.current.onerror = (error) => {
console.error("WebSocket error:", error);
setIsConnected(false);
};
} catch (error) {
console.error("Failed to create WebSocket connection:", error);
setIsConnected(false);
}
};
useEffect(() => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws`;
ws.current = new WebSocket(wsUrl);
ws.current.onopen = () => {
setIsConnected(true);
};
ws.current.onmessage = (event) => {
try {
const message: WSMessage = JSON.parse(event.data);
if (message.type === "user_count") {
setUserCount(message.data.count);
}
onMessage(message);
} catch (error) {
console.error("Failed to parse WebSocket message:", error);
}
};
ws.current.onclose = () => {
setIsConnected(false);
};
ws.current.onerror = () => {
setIsConnected(false);
};
connect();
return () => {
if (reconnectTimer.current) {
clearTimeout(reconnectTimer.current);
}
if (ws.current) {
ws.current.close();
}

View File

@@ -113,15 +113,7 @@
color: var(--muted);
}
.pixel {
transition: all 0.1s ease;
}
.pixel:hover {
transform: scale(1.1);
z-index: 10;
position: relative;
}
/* Entfernt - jetzt unten definiert */
@@ -139,6 +131,33 @@
100% { background-position: 20px 20px; }
}
/* Smooth scrolling für den gesamten Container */
.scroll-smooth {
scroll-behavior: smooth;
}
/* Canvas zoom transitions */
.canvas-zoom {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Geschmeidiges Mausrad-Scrolling */
.canvas-container {
scroll-behavior: smooth;
}
/* Optimierte Pixel-Hover-Effekte */
.pixel {
transition: transform 0.15s ease-out, box-shadow 0.15s ease-out;
}
.pixel:hover {
transform: scale(1.1);
z-index: 10;
position: relative;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
}
/* Toast animations */
.toast-enter {
animation: slideInRight 0.3s ease-out;

View File

@@ -0,0 +1 @@
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#FFFFFF"/></svg>

After

Width:  |  Height:  |  Size: 120 B

View File

@@ -0,0 +1 @@
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#FFFFFF"/></svg>

After

Width:  |  Height:  |  Size: 120 B

View File

@@ -0,0 +1 @@
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#FFFFFF"/><rect x="94" y="69" width="1" height="1" fill="#be0039"/></svg>

After

Width:  |  Height:  |  Size: 177 B

View File

@@ -0,0 +1 @@
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#FFFFFF"/><rect x="94" y="69" width="1" height="1" fill="#be0039"/><rect x="94" y="68" width="1" height="1" fill="#6d001a"/></svg>

After

Width:  |  Height:  |  Size: 234 B

View File

@@ -128,20 +128,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
}
wss.on('connection', (ws) => {
wss.on('connection', (ws, req) => {
console.log(`New WebSocket connection from ${req.socket.remoteAddress}`);
connectedUsers.add(ws);
broadcastUserCount();
ws.on('close', () => {
// Send current user count immediately
ws.send(JSON.stringify({
type: "user_count",
data: { count: connectedUsers.size },
}));
ws.on('close', (code, reason) => {
console.log(`WebSocket disconnected: ${code} ${reason}`);
connectedUsers.delete(ws);
broadcastUserCount();
});
ws.on('error', () => {
ws.on('error', (error) => {
console.error('WebSocket error:', error);
connectedUsers.delete(ws);
broadcastUserCount();
});
ws.on('pong', () => {
// Keep connection alive
});
});
// Keep connections alive with ping/pong
const pingInterval = setInterval(() => {
connectedUsers.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
});
}, 30000);
return httpServer;
}