Modernize collaborative pixel art platform to production-ready architecture
Major refactor from simple HTML/JS app to modern full-stack TypeScript application: ## Architecture Changes - Migrated to monorepo structure with workspaces (backend, frontend, shared) - Backend: Node.js + Express + TypeScript + Socket.IO - Frontend: Next.js 15.5 + React 19 + TypeScript + Tailwind CSS - Shared: Common types and utilities across packages ## Key Features Implemented - Real-time WebSocket collaboration via Socket.IO - Virtual canvas with chunked loading for performance - Modern UI with dark mode and responsive design - Mock database system for easy development (Redis/PostgreSQL compatible) - Comprehensive error handling and rate limiting - User presence and cursor tracking - Infinite canvas support with zoom/pan controls ## Performance Optimizations - Canvas virtualization - only renders visible viewport - Chunked pixel data loading (64x64 pixel chunks) - Optimized WebSocket protocol - Memory-efficient state management with Zustand ## Development Experience - Full TypeScript support across all packages - Hot reload for both frontend and backend - Docker support for production deployment - Comprehensive linting and formatting - Automated development server startup ## Fixed Issues - Corrected start script paths - Updated environment configuration - Fixed ESLint configuration issues - Ensured all dependencies are properly installed - Verified build process works correctly
This commit is contained in:
parent
8e02486a2a
commit
3ce5a97422
69 changed files with 17771 additions and 1589 deletions
10
shared/.eslintrc.json
Normal file
10
shared/.eslintrc.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": ["../.eslintrc.json"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
}
|
||||
20
shared/package.json
Normal file
20
shared/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@gaplace/shared",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared types and utilities for GaPlace",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint src --ext .ts,.tsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
28
shared/src/constants/canvas.ts
Normal file
28
shared/src/constants/canvas.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
export const CANVAS_CONFIG = {
|
||||
DEFAULT_CHUNK_SIZE: 64,
|
||||
MAX_CANVAS_SIZE: 10000,
|
||||
MIN_CANVAS_SIZE: 100,
|
||||
DEFAULT_CANVAS_SIZE: 1000,
|
||||
MAX_ZOOM: 32,
|
||||
MIN_ZOOM: 0.1,
|
||||
DEFAULT_ZOOM: 1,
|
||||
PIXEL_SIZE: 1, // Base pixel size in canvas units
|
||||
} as const;
|
||||
|
||||
export const COLORS = {
|
||||
DEFAULT: '#FFFFFF',
|
||||
PALETTE: [
|
||||
'#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF',
|
||||
'#FFFF00', '#FF00FF', '#00FFFF', '#FFA500', '#800080',
|
||||
'#FFC0CB', '#A52A2A', '#808080', '#90EE90', '#FFB6C1',
|
||||
'#87CEEB', '#DDA0DD', '#98FB98', '#F0E68C', '#FF6347',
|
||||
'#40E0D0'
|
||||
]
|
||||
} as const;
|
||||
|
||||
export const RATE_LIMITS = {
|
||||
PIXELS_PER_MINUTE: 60,
|
||||
PIXELS_PER_HOUR: 1000,
|
||||
CURSOR_UPDATES_PER_SECOND: 10,
|
||||
MAX_CONCURRENT_CHUNKS: 100,
|
||||
} as const;
|
||||
13
shared/src/index.ts
Normal file
13
shared/src/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Types
|
||||
export * from './types/canvas';
|
||||
export * from './types/user';
|
||||
export * from './types/websocket';
|
||||
|
||||
// Constants
|
||||
export * from './constants/canvas';
|
||||
|
||||
// Utils
|
||||
export * from './utils/canvas';
|
||||
|
||||
// Re-export specific functions for convenience
|
||||
export { isValidColor } from './utils/canvas';
|
||||
41
shared/src/types/canvas.ts
Normal file
41
shared/src/types/canvas.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
export interface Pixel {
|
||||
x: number;
|
||||
y: number;
|
||||
color: string;
|
||||
timestamp: number;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface PixelChunk {
|
||||
chunkX: number;
|
||||
chunkY: number;
|
||||
pixels: Map<string, string>; // key: "x,y", value: color
|
||||
lastModified: number;
|
||||
}
|
||||
|
||||
export interface Canvas {
|
||||
id: string;
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
chunkSize: number;
|
||||
isPublic: boolean;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
export interface Viewport {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
export interface CanvasMetadata {
|
||||
totalPixels: number;
|
||||
activeUsers: number;
|
||||
lastActivity: number;
|
||||
version: number;
|
||||
}
|
||||
34
shared/src/types/user.ts
Normal file
34
shared/src/types/user.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
isGuest: boolean;
|
||||
createdAt: number;
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
export interface UserSession {
|
||||
userId: string;
|
||||
sessionId: string;
|
||||
canvasId: string;
|
||||
isActive: boolean;
|
||||
lastActivity: number;
|
||||
cursor?: {
|
||||
x: number;
|
||||
y: number;
|
||||
tool: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserPresence {
|
||||
userId: string;
|
||||
username: string;
|
||||
cursor: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
color: string;
|
||||
tool: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
83
shared/src/types/websocket.ts
Normal file
83
shared/src/types/websocket.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
export enum MessageType {
|
||||
// Canvas operations
|
||||
PLACE_PIXEL = 'PLACE_PIXEL',
|
||||
PIXEL_PLACED = 'PIXEL_PLACED',
|
||||
LOAD_CHUNK = 'LOAD_CHUNK',
|
||||
CHUNK_DATA = 'CHUNK_DATA',
|
||||
|
||||
// User presence
|
||||
USER_JOINED = 'USER_JOINED',
|
||||
USER_LEFT = 'USER_LEFT',
|
||||
CURSOR_MOVE = 'CURSOR_MOVE',
|
||||
USER_LIST = 'USER_LIST',
|
||||
|
||||
// Canvas management
|
||||
CANVAS_INFO = 'CANVAS_INFO',
|
||||
CANVAS_UPDATED = 'CANVAS_UPDATED',
|
||||
|
||||
// System
|
||||
HEARTBEAT = 'HEARTBEAT',
|
||||
ERROR = 'ERROR',
|
||||
RATE_LIMITED = 'RATE_LIMITED'
|
||||
}
|
||||
|
||||
export interface BaseMessage {
|
||||
type: MessageType;
|
||||
timestamp: number;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface PlacePixelMessage extends BaseMessage {
|
||||
type: MessageType.PLACE_PIXEL;
|
||||
x: number;
|
||||
y: number;
|
||||
color: string;
|
||||
canvasId: string;
|
||||
}
|
||||
|
||||
export interface PixelPlacedMessage extends BaseMessage {
|
||||
type: MessageType.PIXEL_PLACED;
|
||||
x: number;
|
||||
y: number;
|
||||
color: string;
|
||||
userId: string;
|
||||
canvasId: string;
|
||||
}
|
||||
|
||||
export interface LoadChunkMessage extends BaseMessage {
|
||||
type: MessageType.LOAD_CHUNK;
|
||||
chunkX: number;
|
||||
chunkY: number;
|
||||
canvasId: string;
|
||||
}
|
||||
|
||||
export interface ChunkDataMessage extends BaseMessage {
|
||||
type: MessageType.CHUNK_DATA;
|
||||
chunkX: number;
|
||||
chunkY: number;
|
||||
pixels: Array<{ x: number; y: number; color: string }>;
|
||||
canvasId: string;
|
||||
}
|
||||
|
||||
export interface CursorMoveMessage extends BaseMessage {
|
||||
type: MessageType.CURSOR_MOVE;
|
||||
x: number;
|
||||
y: number;
|
||||
tool: string;
|
||||
canvasId: string;
|
||||
}
|
||||
|
||||
export interface ErrorMessage extends BaseMessage {
|
||||
type: MessageType.ERROR;
|
||||
message: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export type WebSocketMessage =
|
||||
| PlacePixelMessage
|
||||
| PixelPlacedMessage
|
||||
| LoadChunkMessage
|
||||
| ChunkDataMessage
|
||||
| CursorMoveMessage
|
||||
| ErrorMessage
|
||||
| BaseMessage;
|
||||
44
shared/src/utils/canvas.ts
Normal file
44
shared/src/utils/canvas.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { CANVAS_CONFIG } from '../constants/canvas';
|
||||
|
||||
export function getChunkCoordinates(x: number, y: number, chunkSize = CANVAS_CONFIG.DEFAULT_CHUNK_SIZE) {
|
||||
return {
|
||||
chunkX: Math.floor(x / chunkSize),
|
||||
chunkY: Math.floor(y / chunkSize),
|
||||
};
|
||||
}
|
||||
|
||||
export function getPixelKey(x: number, y: number): string {
|
||||
return `${x},${y}`;
|
||||
}
|
||||
|
||||
export function parsePixelKey(key: string): { x: number; y: number } {
|
||||
const [x, y] = key.split(',').map(Number);
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
export function getChunkKey(chunkX: number, chunkY: number): string {
|
||||
return `${chunkX},${chunkY}`;
|
||||
}
|
||||
|
||||
export function parseChunkKey(key: string): { chunkX: number; chunkY: number } {
|
||||
const [chunkX, chunkY] = key.split(',').map(Number);
|
||||
return { chunkX, chunkY };
|
||||
}
|
||||
|
||||
export function getChunkBounds(chunkX: number, chunkY: number, chunkSize = CANVAS_CONFIG.DEFAULT_CHUNK_SIZE) {
|
||||
return {
|
||||
minX: chunkX * chunkSize,
|
||||
minY: chunkY * chunkSize,
|
||||
maxX: (chunkX + 1) * chunkSize - 1,
|
||||
maxY: (chunkY + 1) * chunkSize - 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function isValidColor(color: string): boolean {
|
||||
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
||||
return hexRegex.test(color);
|
||||
}
|
||||
|
||||
export function clampCoordinate(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
19
shared/tsconfig.json
Normal file
19
shared/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue