<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>k/place</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            display: flex;
            flex-direction: column;
            height: 100vh;
            background-color: #f0f0f0;
        }
        header {
            background-color: #333;
            color: white;
            padding: 10px;
            text-align: center;
        }
        main {
            display: flex;
            flex: 1;
            overflow: hidden;
        }
        #canvasContainer {
            flex: 1;
            overflow: hidden;
            position: relative;
        }
        #grid {
            position: absolute;
            cursor: crosshair;
        }
        #grid:active {
            cursor: grabbing;
        }
        #minimap {
            position: absolute;
            bottom: 10px;
            right: 10px;
            border: 2px solid #333;
        }
        #controls {
            width: 200px;
            padding: 10px;
            background-color: #ddd;
        }
        .color-btn {
            width: 30px;
            height: 30px;
            margin: 2px;
            border: none;
            cursor: pointer;
        }
        #userInfo {
            margin-top: 10px;
        }
        #pixelInfo {
            position: absolute;
            background-color: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 5px;
            border-radius: 3px;
            display: none;
        }
    </style>
</head>
<body>
    <header>
        <h1>k/place</h1>
        <p>Collaborate on an infinite canvas, one pixel at a time!</p>
    </header>
    <main>
        <div id="canvasContainer">
            <canvas id="grid"></canvas>
            <canvas id="minimap"></canvas>
            <div id="pixelInfo"></div>
        </div>
        <div id="controls">
            <div id="colorPalette"></div>
            <div id="userInfo">
                <input type="text" id="username" placeholder="Enter username">
                <button id="setUsername">Set Username</button>
                <p>Online users: <span id="onlineUsers">0</span></p>
            </div>
        </div>
    </main>
    <script src="/socket.io/socket.io.js"></script>
    <script>
        const socket = io();
const canvas = document.getElementById('grid');
const ctx = canvas.getContext('2d');
const bufferCanvas = document.createElement('canvas');
const bufferCtx = bufferCanvas.getContext('2d');
const minimap = document.getElementById('minimap');
const minimapCtx = minimap.getContext('2d');
const colorPalette = document.getElementById('colorPalette');
const pixelInfo = document.getElementById('pixelInfo');
const usernameInput = document.getElementById('username');
const setUsernameBtn = document.getElementById('setUsername');
const onlineUsersSpan = document.getElementById('onlineUsers');
const CHUNK_SIZE = 16;
const pixelSize = 10;
const colors = ['#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#00FFFF', '#FF00FF'];
let offsetX = 0, offsetY = 0;
let zoom = 1;
let username = 'Anonymous';
let chunks = new Map();

function resizeCanvas() {
    canvas.width = window.innerWidth - 220;
    canvas.height = window.innerHeight - 100;
    bufferCanvas.width = canvas.width;
    bufferCanvas.height = canvas.height;
    minimap.width = 200;
    minimap.height = 200;
    loadChunks();
}

resizeCanvas();
window.addEventListener('resize', resizeCanvas);

colors.forEach(color => {
    const btn = document.createElement('button');
    btn.className = 'color-btn';
    btn.style.backgroundColor = color;
    btn.onclick = () => selectedColor = color;
    colorPalette.appendChild(btn);
});

let selectedColor = colors[0];

function drawPixel(chunkKey, x, y, color) {
    const [chunkX, chunkY] = chunkKey.split(',').map(Number);
    const pixelX = chunkX * CHUNK_SIZE + x;
    const pixelY = chunkY * CHUNK_SIZE + y;
    bufferCtx.fillStyle = color;
    bufferCtx.fillRect(
        Math.round((pixelX * pixelSize - offsetX) * zoom),
        Math.round((pixelY * pixelSize - offsetY) * zoom),
        Math.ceil(pixelSize * zoom),
        Math.ceil(pixelSize * zoom)
    );
}

function updateCanvas() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    bufferCtx.clearRect(0, 0, bufferCanvas.width, bufferCanvas.height);

    const visibleChunks = getVisibleChunks();
    visibleChunks.forEach(chunkKey => {
        const chunkData = chunks.get(chunkKey);
        if (chunkData) {
            Object.entries(chunkData).forEach(([pixelKey, data]) => {
                const [x, y] = pixelKey.split(',').map(Number);
                drawPixel(chunkKey, x, y, data.color);
            });
        }
    });

    ctx.drawImage(bufferCanvas, 0, 0);
    updateMinimap();
}

function updateMinimap() {
    minimapCtx.clearRect(0, 0, minimap.width, minimap.height);

    const visibleChunks = getVisibleChunks();
    visibleChunks.forEach(chunkKey => {
        const chunkData = chunks.get(chunkKey);
        if (chunkData) {
            const [chunkX, chunkY] = chunkKey.split(',').map(Number);
            Object.entries(chunkData).forEach(([pixelKey, data]) => {
                const [x, y] = pixelKey.split(',').map(Number);
                const pixelX = chunkX * CHUNK_SIZE + x;
                const pixelY = chunkY * CHUNK_SIZE + y;
                minimapCtx.fillStyle = data.color;
                minimapCtx.fillRect(
                    (pixelX * minimap.width) / (canvas.width / zoom / pixelSize),
                    (pixelY * minimap.height) / (canvas.height / zoom / pixelSize),
                    1,
                    1
                );
            });
        }
    });

    minimapCtx.strokeStyle = 'red';
    minimapCtx.strokeRect(
        (offsetX / (canvas.width / zoom)) * minimap.width,
        (offsetY / (canvas.height / zoom)) * minimap.height,
        (canvas.width / (canvas.width / zoom)) * minimap.width,
        (canvas.height / (canvas.height / zoom)) * minimap.height
    );
}

function getVisibleChunks() {
    const startX = Math.floor(offsetX / (CHUNK_SIZE * pixelSize * zoom));
    const startY = Math.floor(offsetY / (CHUNK_SIZE * pixelSize * zoom));
    const endX = Math.ceil((offsetX + canvas.width / zoom) / (CHUNK_SIZE * pixelSize));
    const endY = Math.ceil((offsetY + canvas.height / zoom) / (CHUNK_SIZE * pixelSize));

    const visibleChunks = [];
    for (let x = startX; x <= endX; x++) {
        for (let y = startY; y <= endY; y++) {
            visibleChunks.push(`${x},${y}`);
        }
    }
    return visibleChunks;
}

function loadChunks() {
    const visibleChunks = getVisibleChunks();
    const chunksToLoad = visibleChunks.filter(chunk => !chunks.has(chunk));
    if (chunksToLoad.length > 0) {
        socket.emit('getChunks', chunksToLoad);
    }
    updateCanvas();
}

socket.on('chunkData', (chunkData) => {
    Object.entries(chunkData).forEach(([chunkKey, pixelData]) => {
        chunks.set(chunkKey, pixelData);
    });
    updateCanvas();
});

socket.on('pixelUpdate', (data) => {
    const { chunkKey, x, y, color } = data;
    let chunkData = chunks.get(chunkKey);
    if (!chunkData) {
        chunkData = {};
        chunks.set(chunkKey, chunkData);
    }
    chunkData[`${x},${y}`] = { color };
    updateCanvas();
});

socket.on('onlineUsers', (count) => {
    onlineUsersSpan.textContent = count;
});

function placePixel(event) {
    const rect = canvas.getBoundingClientRect();
    const x = Math.floor((event.clientX - rect.left + offsetX) / (pixelSize * zoom));
    const y = Math.floor((event.clientY - rect.top + offsetY) / (pixelSize * zoom));
    socket.emit('placePixel', { x, y, color: selectedColor, username });
}

canvas.addEventListener('mousedown', (event) => {
    if (event.button === 0) {
        placePixel(event);
        canvas.addEventListener('mousemove', placePixel);
    } else if (event.button === 2) {
        isDragging = true;
        lastX = event.clientX;
        lastY = event.clientY;
    }
});

canvas.addEventListener('mouseup', () => {
    canvas.removeEventListener('mousemove', placePixel);
    isDragging = false;
});

canvas.addEventListener('contextmenu', (event) => {
    event.preventDefault();
});

let isDragging = false;
let lastX, lastY;

canvas.addEventListener('mousemove', (event) => {
    if (isDragging) {
        const deltaX = event.clientX - lastX;
        const deltaY = event.clientY - lastY;
        offsetX -= deltaX / zoom;
        offsetY -= deltaY / zoom;
        lastX = event.clientX;
        lastY = event.clientY;

        loadChunks();
    }

    const rect = canvas.getBoundingClientRect();
    const x = Math.floor((event.clientX - rect.left + offsetX) / (pixelSize * zoom));
    const y = Math.floor((event.clientY - rect.top + offsetY) / (pixelSize * zoom));
    socket.emit('getPixelInfo', { x, y });
});

canvas.addEventListener('wheel', (event) => {
    event.preventDefault();
    const rect = canvas.getBoundingClientRect();
    const mouseX = event.clientX - rect.left;
    const mouseY = event.clientY - rect.top;

    const zoomPoint = {
        x: (mouseX + offsetX) / zoom,
        y: (mouseY + offsetY) / zoom
    };

    const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1;
    zoom *= zoomFactor;
    zoom = Math.max(0.1, Math.min(zoom, 10));

    offsetX = zoomPoint.x * zoom - mouseX;
    offsetY = zoomPoint.y * zoom - mouseY;

    loadChunks();
});

socket.on('pixelInfo', (info) => {
    if (info) {
        pixelInfo.style.display = 'block';
        pixelInfo.style.left = `${event.clientX + 10}px`;
        pixelInfo.style.top = `${event.clientY + 10}px`;
        pixelInfo.textContent = `Placed by: ${info.username}\nColor: ${info.color}\nPosition: (${info.x}, ${info.y})\nTime: ${new Date(info.time).toLocaleString()}`;
    } else {
        pixelInfo.style.display = 'none';
    }
});

setUsernameBtn.addEventListener('click', () => {
    username = usernameInput.value || 'Anonymous';
    socket.emit('setUsername', username);
});

loadChunks();
    </script>
</body>
</html>

const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);

const CHUNK_SIZE = 16; // Easily changeable chunk size
const grid = new Map();
let onlineUsers = 0;

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

function getChunkKey(x, y) {
  const chunkX = Math.floor(x / CHUNK_SIZE);
  const chunkY = Math.floor(y / CHUNK_SIZE);
  return `${chunkX},${chunkY}`;
}

io.on('connection', (socket) => {
  console.log('A user connected');
  onlineUsers++;
  io.emit('onlineUsers', onlineUsers);

  socket.on('getChunks', (chunks) => {
    const chunkData = {};
    chunks.forEach(chunk => {
      const [chunkX, chunkY] = chunk.split(',').map(Number);
      chunkData[chunk] = {};
      for (let x = 0; x < CHUNK_SIZE; x++) {
        for (let y = 0; y < CHUNK_SIZE; y++) {
          const pixelX = chunkX * CHUNK_SIZE + x;
          const pixelY = chunkY * CHUNK_SIZE + y;
          const key = `${pixelX},${pixelY}`;
          const pixelData = grid.get(key);
          if (pixelData) {
            chunkData[chunk][`${x},${y}`] = pixelData;
          }
        }
      }
    });
    socket.emit('chunkData', chunkData);
  });

  socket.on('placePixel', (data) => {
    const { x, y, color, username } = data;
    const key = `${x},${y}`;
    const pixelData = { color, username, time: Date.now() };
    grid.set(key, pixelData);
    const chunkKey = getChunkKey(x, y);
    io.emit('pixelUpdate', { chunkKey, x: x % CHUNK_SIZE, y: y % CHUNK_SIZE, color });
  });

  socket.on('getPixelInfo', (data) => {
    const { x, y } = data;
    const key = `${x},${y}`;
    const pixelData = grid.get(key);
    if (pixelData) {
      socket.emit('pixelInfo', { ...pixelData, x, y });
    } else {
      socket.emit('pixelInfo', null);
    }
  });

  socket.on('setUsername', (username) => {
    socket.username = username;
  });

  socket.on('disconnect', () => {
    console.log('User disconnected');
    onlineUsers--;
    io.emit('onlineUsers', onlineUsers);
  });
});

const PORT = process.env.PORT || 3000;
http.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

find and fix why the zoom is not being properly transformed, when zoomed in or out the pixel should always be placed at the position where the mouse is on that pixel on the canvas visually, based on zoom.