import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { CANVAS_CONFIG, COLORS } from '@gaplace/shared'; interface PixelData { x: number; y: number; color: string; timestamp?: number; userId?: string; username?: string; } interface PixelInfo { color: string; userId?: string; username?: string; timestamp?: number; } interface Chunk { chunkX: number; chunkY: number; pixels: Map; // key: "x,y", value: pixel info isLoaded: boolean; lastModified: number; } interface Viewport { x: number; y: number; width: number; height: number; zoom: number; } interface CanvasState { // Canvas data canvasId: string; chunks: Map; // key: "chunkX,chunkY" // Viewport viewport: Viewport; // Tools selectedColor: string; selectedTool: 'pixel' | 'fill' | 'eyedropper'; brushSize: number; // UI state isLoading: boolean; isPanning: boolean; showGrid: boolean; showCursors: boolean; // User presence activeUsers: string[]; userCursors: Map; // Stats totalPixels: number; userPixels: number; // Actions setCanvasId: (id: string) => void; setPixel: (x: number, y: number, color: string, userId?: string, username?: string) => void; loadChunk: (chunkX: number, chunkY: number, pixels: PixelData[]) => void; setViewport: (viewport: Partial) => void; setZoom: (zoom: number, centerX?: number, centerY?: number) => void; pan: (deltaX: number, deltaY: number) => void; setSelectedColor: (color: string) => void; setSelectedTool: (tool: 'pixel' | 'fill' | 'eyedropper') => void; setBrushSize: (size: number) => void; setUserCursor: (userId: string, x: number, y: number, username: string, color: string) => void; removeUserCursor: (userId: string) => void; setActiveUsers: (users: string[]) => void; setStats: (totalPixels: number, userPixels?: number) => void; getPixelAt: (x: number, y: number) => string | null; getPixelInfo: (x: number, y: number) => PixelInfo | null; getChunkKey: (chunkX: number, chunkY: number) => string; getPixelKey: (x: number, y: number) => string; getChunkCoordinates: (x: number, y: number) => { chunkX: number; chunkY: number }; } export const useCanvasStore = create()( devtools( (set, get) => ({ // Initial state canvasId: 'default', chunks: new Map(), viewport: { x: 0, y: 0, width: 1920, height: 1080, zoom: 1, }, selectedColor: COLORS.PALETTE[0], selectedTool: 'pixel', brushSize: 1, isLoading: false, isPanning: false, showGrid: true, showCursors: true, activeUsers: [], userCursors: new Map(), totalPixels: 0, userPixels: 0, // Actions setCanvasId: (id) => set({ canvasId: id }), setPixel: (x, y, color, userId, username) => { const state = get(); const { chunkX, chunkY } = state.getChunkCoordinates(x, y); const chunkKey = state.getChunkKey(chunkX, chunkY); const pixelKey = state.getPixelKey( x % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE, y % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE ); const chunks = new Map(state.chunks); let chunk = chunks.get(chunkKey); if (!chunk) { chunk = { chunkX, chunkY, pixels: new Map(), isLoaded: true, lastModified: Date.now(), }; } const pixelInfo: PixelInfo = { color, userId, username, timestamp: Date.now() }; chunk.pixels.set(pixelKey, pixelInfo); chunk.lastModified = Date.now(); chunks.set(chunkKey, chunk); set({ chunks }); }, loadChunk: (chunkX, chunkY, pixels) => { const state = get(); const chunkKey = state.getChunkKey(chunkX, chunkY); const chunks = new Map(state.chunks); const pixelMap = new Map(); pixels.forEach((pixel) => { const localX = pixel.x % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE; const localY = pixel.y % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE; const pixelKey = state.getPixelKey(localX, localY); pixelMap.set(pixelKey, { color: pixel.color, userId: pixel.userId, username: pixel.username, timestamp: pixel.timestamp }); }); const chunk: Chunk = { chunkX, chunkY, pixels: pixelMap, isLoaded: true, lastModified: Date.now(), }; chunks.set(chunkKey, chunk); set({ chunks }); }, setViewport: (newViewport) => { const state = get(); set({ viewport: { ...state.viewport, ...newViewport }, }); }, setZoom: (zoom, centerX, centerY) => { const state = get(); const clampedZoom = Math.max(CANVAS_CONFIG.MIN_ZOOM, Math.min(CANVAS_CONFIG.MAX_ZOOM, zoom)); let newViewport = { ...state.viewport, zoom: clampedZoom }; // If center point is provided, zoom towards that point if (centerX !== undefined && centerY !== undefined) { const BASE_PIXEL_SIZE = 32; const currentPixelSize = BASE_PIXEL_SIZE * state.viewport.zoom; const newPixelSize = BASE_PIXEL_SIZE * clampedZoom; // Calculate what canvas coordinate is at the center point const canvasCenterX = (centerX + state.viewport.x) / currentPixelSize; const canvasCenterY = (centerY + state.viewport.y) / currentPixelSize; // Calculate new viewport to keep same canvas point at center newViewport.x = canvasCenterX * newPixelSize - centerX; newViewport.y = canvasCenterY * newPixelSize - centerY; } set({ viewport: newViewport }); }, pan: (deltaX, deltaY) => { const state = get(); set({ viewport: { ...state.viewport, x: state.viewport.x + deltaX, y: state.viewport.y + deltaY, }, }); }, setSelectedColor: (color) => set({ selectedColor: color }), setSelectedTool: (tool) => set({ selectedTool: tool }), setBrushSize: (size) => set({ brushSize: Math.max(1, Math.min(10, size)) }), setUserCursor: (userId, x, y, username, color) => { const state = get(); const userCursors = new Map(state.userCursors); userCursors.set(userId, { x, y, username, color }); set({ userCursors }); }, removeUserCursor: (userId) => { const state = get(); const userCursors = new Map(state.userCursors); userCursors.delete(userId); set({ userCursors }); }, setActiveUsers: (users) => set({ activeUsers: users }), setStats: (totalPixels, userPixels) => set({ totalPixels, ...(userPixels !== undefined && { userPixels }) }), getPixelAt: (x, y) => { const state = get(); const { chunkX, chunkY } = state.getChunkCoordinates(x, y); const chunkKey = state.getChunkKey(chunkX, chunkY); const chunk = state.chunks.get(chunkKey); if (!chunk) return null; const localX = x % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE; const localY = y % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE; const pixelKey = state.getPixelKey(localX, localY); return chunk.pixels.get(pixelKey)?.color || null; }, getPixelInfo: (x, y) => { const state = get(); const { chunkX, chunkY } = state.getChunkCoordinates(x, y); const chunkKey = state.getChunkKey(chunkX, chunkY); const chunk = state.chunks.get(chunkKey); if (!chunk) return null; const localX = x % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE; const localY = y % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE; const pixelKey = state.getPixelKey(localX, localY); return chunk.pixels.get(pixelKey) || null; }, getChunkKey: (chunkX, chunkY) => `${chunkX},${chunkY}`, getPixelKey: (x, y) => `${x},${y}`, getChunkCoordinates: (x, y) => ({ chunkX: Math.floor(x / CANVAS_CONFIG.DEFAULT_CHUNK_SIZE), chunkY: Math.floor(y / CANVAS_CONFIG.DEFAULT_CHUNK_SIZE), }), }), { name: 'canvas-store' } ) );