594 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			594 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!DOCTYPE html>
 | 
						||
<html lang="en">
 | 
						||
 | 
						||
<head>
 | 
						||
    <meta charset="UTF-8">
 | 
						||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
						||
    <title>Infinite Place</title>
 | 
						||
    <style>
 | 
						||
        /* Base styles */
 | 
						||
        * {
 | 
						||
            box-sizing: border-box;
 | 
						||
        }
 | 
						||
 | 
						||
        body {
 | 
						||
            margin: 0;
 | 
						||
            padding: 0;
 | 
						||
            overflow: hidden;
 | 
						||
            background: #f0f0f0;
 | 
						||
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 | 
						||
        }
 | 
						||
 | 
						||
        /* Canvas */
 | 
						||
        #canvas {
 | 
						||
            display: block;
 | 
						||
            cursor: crosshair;
 | 
						||
        }
 | 
						||
 | 
						||
        /* UI Components */
 | 
						||
        .ui-panel {
 | 
						||
            position: fixed;
 | 
						||
            background: rgba(0, 0, 0, 0.8);
 | 
						||
            color: white;
 | 
						||
            padding: 12px;
 | 
						||
            border-radius: 8px;
 | 
						||
            font-size: 12px;
 | 
						||
            z-index: 1000;
 | 
						||
            backdrop-filter: blur(4px);
 | 
						||
        }
 | 
						||
 | 
						||
        .info-panel {
 | 
						||
            top: 16px;
 | 
						||
            left: 16px;
 | 
						||
        }
 | 
						||
 | 
						||
        .controls-panel {
 | 
						||
            top: 16px;
 | 
						||
            right: 16px;
 | 
						||
        }
 | 
						||
 | 
						||
        .zoom-panel {
 | 
						||
            bottom: 20px;
 | 
						||
            right: 20px;
 | 
						||
            text-align: center;
 | 
						||
        }
 | 
						||
 | 
						||
        /* Buttons */
 | 
						||
        .btn {
 | 
						||
            background: #4CAF50;
 | 
						||
            color: white;
 | 
						||
            border: none;
 | 
						||
            padding: 8px 12px;
 | 
						||
            border-radius: 4px;
 | 
						||
            cursor: pointer;
 | 
						||
            font-size: 12px;
 | 
						||
            transition: background-color 0.2s;
 | 
						||
        }
 | 
						||
 | 
						||
        .btn:hover {
 | 
						||
            background: #45a049;
 | 
						||
        }
 | 
						||
 | 
						||
        .btn-blue {
 | 
						||
            background: #2196F3;
 | 
						||
            font-weight: bold;
 | 
						||
            font-size: 14px;
 | 
						||
            min-width: 36px;
 | 
						||
            height: 36px;
 | 
						||
        }
 | 
						||
 | 
						||
        .btn-blue:hover {
 | 
						||
            background: #1976D2;
 | 
						||
        }
 | 
						||
 | 
						||
        .zoom-level {
 | 
						||
            margin-top: 8px;
 | 
						||
            font-size: 10px;
 | 
						||
            opacity: 0.8;
 | 
						||
        }
 | 
						||
 | 
						||
        /* Status indicators */
 | 
						||
        .status-dot {
 | 
						||
            display: inline-block;
 | 
						||
            width: 8px;
 | 
						||
            height: 8px;
 | 
						||
            border-radius: 50%;
 | 
						||
            margin-right: 6px;
 | 
						||
        }
 | 
						||
 | 
						||
        .status-connected {
 | 
						||
            background: #4CAF50;
 | 
						||
        }
 | 
						||
 | 
						||
        .status-disconnected {
 | 
						||
            background: #f44336;
 | 
						||
        }
 | 
						||
 | 
						||
        .status-connecting {
 | 
						||
            background: #ff9800;
 | 
						||
        }
 | 
						||
    </style>
 | 
						||
</head>
 | 
						||
 | 
						||
<body>
 | 
						||
    {{ .csrfField }}
 | 
						||
    <canvas id="canvas"></canvas>
 | 
						||
 | 
						||
    <div class="ui-panel info-panel">
 | 
						||
        <div>Position: <span id="position">0, 0</span></div>
 | 
						||
        <div>
 | 
						||
            <span class="status-dot" id="connectionStatus"></span>
 | 
						||
            <span id="connectionText">Connecting...</span>
 | 
						||
        </div>
 | 
						||
        <div style="margin-top: 8px; font-size: 11px; opacity: 0.8;">
 | 
						||
            Click cells to toggle • Arrow keys to move
 | 
						||
        </div>
 | 
						||
    </div>
 | 
						||
 | 
						||
    <div class="ui-panel controls-panel">
 | 
						||
        <button class="btn" onclick="gridCanvas.resetView()">Reset View</button>
 | 
						||
        <button class="btn" onclick="gridCanvas.clearAll()">Clear All</button>
 | 
						||
    </div>
 | 
						||
 | 
						||
    <div class="ui-panel zoom-panel">
 | 
						||
        <button class="btn btn-blue" onclick="gridCanvas.zoomIn()">+</button>
 | 
						||
        <button class="btn btn-blue" onclick="gridCanvas.zoomOut()">−</button>
 | 
						||
        <div class="zoom-level">Zoom: <span id="zoomLevel">100%</span></div>
 | 
						||
    </div>
 | 
						||
 | 
						||
    <script>
 | 
						||
        class GridCanvas {
 | 
						||
            constructor(canvasId) {
 | 
						||
                // DOM elements
 | 
						||
                this.canvas = document.getElementById(canvasId);
 | 
						||
                this.ctx = this.canvas.getContext('2d');
 | 
						||
                this.positionElement = document.getElementById('position');
 | 
						||
                this.zoomElement = document.getElementById('zoomLevel');
 | 
						||
                this.connectionStatus = document.getElementById('connectionStatus');
 | 
						||
                this.connectionText = document.getElementById('connectionText');
 | 
						||
 | 
						||
                // Configuration
 | 
						||
                this.config = {
 | 
						||
                    cellSize: 20,
 | 
						||
                    gridColor: '#ddd',
 | 
						||
                    filledColor: '#333',
 | 
						||
                    emptyColor: '#fff',
 | 
						||
                    moveSpeed: 50,
 | 
						||
                    zoomStep: 0.1,
 | 
						||
                    minZoom: 0.1,
 | 
						||
                    maxZoom: 2.0,
 | 
						||
                    debounceDelay: 100,
 | 
						||
                };
 | 
						||
 | 
						||
                // State
 | 
						||
                this.view = { x: 0, y: 0 };
 | 
						||
                this.scale = 1;
 | 
						||
                this.filledCells = new Set();
 | 
						||
                this.isDragging = false;
 | 
						||
                this.lastMousePos = { x: 0, y: 0 };
 | 
						||
 | 
						||
                // Server connection
 | 
						||
                this.eventSource = null;
 | 
						||
                this.csrfToken = this.getCSRFToken();
 | 
						||
 | 
						||
                this.init();
 | 
						||
            }
 | 
						||
 | 
						||
            init() {
 | 
						||
                this.setupEventListeners();
 | 
						||
                this.resizeCanvas();
 | 
						||
                this.updateSSEConnection();
 | 
						||
            }
 | 
						||
 | 
						||
            getCSRFToken() {
 | 
						||
                const tokenElement = document.getElementsByName("gorilla.csrf.Token")[0];
 | 
						||
                return tokenElement ? tokenElement.value : null;
 | 
						||
            }
 | 
						||
 | 
						||
            setupEventListeners() {
 | 
						||
                // Canvas events
 | 
						||
                this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
 | 
						||
                this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
 | 
						||
                this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
 | 
						||
                this.canvas.addEventListener('wheel', this.handleWheel.bind(this));
 | 
						||
                this.canvas.addEventListener('contextmenu', e => e.preventDefault());
 | 
						||
 | 
						||
                // Window events
 | 
						||
                window.addEventListener('resize', this.resizeCanvas.bind(this));
 | 
						||
                window.addEventListener('keydown', this.handleKeyDown.bind(this));
 | 
						||
                window.addEventListener('beforeunload', this.cleanup.bind(this));
 | 
						||
            }
 | 
						||
 | 
						||
            // Coordinate conversion utilities
 | 
						||
            screenToGrid(screenX, screenY) {
 | 
						||
                const worldX = (screenX / this.scale) + this.view.x;
 | 
						||
                const worldY = (screenY / this.scale) + this.view.y;
 | 
						||
                return {
 | 
						||
                    x: Math.floor(worldX / this.config.cellSize),
 | 
						||
                    y: Math.floor(worldY / this.config.cellSize)
 | 
						||
                };
 | 
						||
            }
 | 
						||
 | 
						||
            gridToScreen(gridX, gridY) {
 | 
						||
                const worldX = gridX * this.config.cellSize;
 | 
						||
                const worldY = gridY * this.config.cellSize;
 | 
						||
                return {
 | 
						||
                    x: (worldX - this.view.x) * this.scale,
 | 
						||
                    y: (worldY - this.view.y) * this.scale
 | 
						||
                };
 | 
						||
            }
 | 
						||
 | 
						||
            getCellKey(gridX, gridY) {
 | 
						||
                return `${gridX},${gridY}`;
 | 
						||
            }
 | 
						||
 | 
						||
            // Drawing methods
 | 
						||
            resizeCanvas() {
 | 
						||
                this.canvas.width = window.innerWidth;
 | 
						||
                this.canvas.height = window.innerHeight;
 | 
						||
                this.draw();
 | 
						||
                this.updateSSEConnection();
 | 
						||
            }
 | 
						||
 | 
						||
            draw() {
 | 
						||
                this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
 | 
						||
 | 
						||
                const bounds = this.getVisibleGridBounds();
 | 
						||
                this.drawGrid(bounds);
 | 
						||
                this.drawFilledCells(bounds);
 | 
						||
                this.updateUI();
 | 
						||
            }
 | 
						||
 | 
						||
            getVisibleGridBounds() {
 | 
						||
                return {
 | 
						||
                    startX: Math.floor(this.view.x / this.config.cellSize) - 1,
 | 
						||
                    endX: Math.floor((this.view.x + this.canvas.width / this.scale) / this.config.cellSize) + 1,
 | 
						||
                    startY: Math.floor(this.view.y / this.config.cellSize) - 1,
 | 
						||
                    endY: Math.floor((this.view.y + this.canvas.height / this.scale) / this.config.cellSize) + 1
 | 
						||
                };
 | 
						||
            }
 | 
						||
 | 
						||
            drawGrid(bounds) {
 | 
						||
                this.ctx.strokeStyle = this.config.gridColor;
 | 
						||
                this.ctx.lineWidth = Math.max(0.2, Math.min(3, 0.8 * this.scale));
 | 
						||
 | 
						||
                // Vertical lines
 | 
						||
                for (let x = bounds.startX; x <= bounds.endX; x++) {
 | 
						||
                    const screenPos = this.gridToScreen(x, 0);
 | 
						||
                    this.ctx.beginPath();
 | 
						||
                    this.ctx.moveTo(screenPos.x, 0);
 | 
						||
                    this.ctx.lineTo(screenPos.x, this.canvas.height);
 | 
						||
                    this.ctx.stroke();
 | 
						||
                }
 | 
						||
 | 
						||
                // Horizontal lines
 | 
						||
                for (let y = bounds.startY; y <= bounds.endY; y++) {
 | 
						||
                    const screenPos = this.gridToScreen(0, y);
 | 
						||
                    this.ctx.beginPath();
 | 
						||
                    this.ctx.moveTo(0, screenPos.y);
 | 
						||
                    this.ctx.lineTo(this.canvas.width, screenPos.y);
 | 
						||
                    this.ctx.stroke();
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            drawFilledCells(bounds) {
 | 
						||
                this.ctx.fillStyle = this.config.filledColor;
 | 
						||
 | 
						||
                for (let x = bounds.startX; x <= bounds.endX; x++) {
 | 
						||
                    for (let y = bounds.startY; y <= bounds.endY; y++) {
 | 
						||
                        const key = this.getCellKey(x, y);
 | 
						||
                        if (this.filledCells.has(key)) {
 | 
						||
                            const screenPos = this.gridToScreen(x, y);
 | 
						||
                            this.ctx.fillRect(
 | 
						||
                                screenPos.x,
 | 
						||
                                screenPos.y,
 | 
						||
                                this.config.cellSize * this.scale,
 | 
						||
                                this.config.cellSize * this.scale
 | 
						||
                            );
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            updateUI() {
 | 
						||
                this.positionElement.textContent = `${Math.round(this.view.x)}, ${Math.round(this.view.y)}`;
 | 
						||
                this.zoomElement.textContent = `${Math.round(this.scale * 100)}%`;
 | 
						||
            }
 | 
						||
 | 
						||
            // Event handlers
 | 
						||
            handleMouseDown(e) {
 | 
						||
                const rect = this.canvas.getBoundingClientRect();
 | 
						||
                this.lastMousePos = {
 | 
						||
                    x: e.clientX - rect.left,
 | 
						||
                    y: e.clientY - rect.top
 | 
						||
                };
 | 
						||
                this.isDragging = false;
 | 
						||
            }
 | 
						||
 | 
						||
            handleMouseMove(e) {
 | 
						||
                if (e.buttons === 1) { // Left mouse button pressed
 | 
						||
                    const rect = this.canvas.getBoundingClientRect();
 | 
						||
                    const currentPos = {
 | 
						||
                        x: e.clientX - rect.left,
 | 
						||
                        y: e.clientY - rect.top
 | 
						||
                    };
 | 
						||
 | 
						||
                    if (!this.isDragging) {
 | 
						||
                        // Check if mouse moved enough to start dragging
 | 
						||
                        const distance = Math.sqrt(
 | 
						||
                            Math.pow(currentPos.x - this.lastMousePos.x, 2) +
 | 
						||
                            Math.pow(currentPos.y - this.lastMousePos.y, 2)
 | 
						||
                        );
 | 
						||
                        if (distance > 5) {
 | 
						||
                            this.isDragging = true;
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
 | 
						||
                    if (this.isDragging) {
 | 
						||
                        const deltaX = (currentPos.x - this.lastMousePos.x) / this.scale;
 | 
						||
                        const deltaY = (currentPos.y - this.lastMousePos.y) / this.scale;
 | 
						||
 | 
						||
                        this.view.x -= deltaX;
 | 
						||
                        this.view.y -= deltaY;
 | 
						||
 | 
						||
                        this.draw();
 | 
						||
                        this.debounceSSEUpdate();
 | 
						||
                        this.lastMousePos = currentPos;
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            handleMouseUp(e) {
 | 
						||
                if (!this.isDragging) {
 | 
						||
                    // Single click - toggle cell
 | 
						||
                    const rect = this.canvas.getBoundingClientRect();
 | 
						||
                    const mousePos = {
 | 
						||
                        x: e.clientX - rect.left,
 | 
						||
                        y: e.clientY - rect.top
 | 
						||
                    };
 | 
						||
                    this.toggleCell(mousePos.x, mousePos.y);
 | 
						||
                }
 | 
						||
                this.isDragging = false;
 | 
						||
            }
 | 
						||
 | 
						||
            handleWheel(e) {
 | 
						||
                e.preventDefault();
 | 
						||
                const factor = e.deltaY > 0 ? -this.config.zoomStep : this.config.zoomStep;
 | 
						||
                this.zoomAt(e.offsetX, e.offsetY, factor);
 | 
						||
            }
 | 
						||
 | 
						||
            handleKeyDown(e) {
 | 
						||
                switch (e.key) {
 | 
						||
                    case 'ArrowLeft':
 | 
						||
                        this.view.x -= this.config.moveSpeed;
 | 
						||
                        this.draw();
 | 
						||
                        this.debounceSSEUpdate();
 | 
						||
                        break;
 | 
						||
                    case 'ArrowRight':
 | 
						||
                        this.view.x += this.config.moveSpeed;
 | 
						||
                        this.draw();
 | 
						||
                        this.debounceSSEUpdate();
 | 
						||
                        break;
 | 
						||
                    case 'ArrowUp':
 | 
						||
                        this.view.y -= this.config.moveSpeed;
 | 
						||
                        this.draw();
 | 
						||
                        this.debounceSSEUpdate();
 | 
						||
                        break;
 | 
						||
                    case 'ArrowDown':
 | 
						||
                        this.view.y += this.config.moveSpeed;
 | 
						||
                        this.draw();
 | 
						||
                        this.debounceSSEUpdate();
 | 
						||
                        break;
 | 
						||
                    default:
 | 
						||
                        return;
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            // Cell manipulation
 | 
						||
            toggleCell(screenX, screenY) {
 | 
						||
                const gridPos = this.screenToGrid(screenX, screenY);
 | 
						||
                const key = this.getCellKey(gridPos.x, gridPos.y);
 | 
						||
 | 
						||
                const newState = !this.filledCells.has(key);
 | 
						||
 | 
						||
                if (newState) {
 | 
						||
                    this.filledCells.add(key);
 | 
						||
                } else {
 | 
						||
                    this.filledCells.delete(key);
 | 
						||
                }
 | 
						||
 | 
						||
                this.draw();
 | 
						||
                this.sendCellState(gridPos.x, gridPos.y, newState);
 | 
						||
            }
 | 
						||
 | 
						||
            // Zoom controls
 | 
						||
            zoomAt(screenX, screenY, factor) {
 | 
						||
                const newScale = Math.max(this.config.minZoom,
 | 
						||
                    Math.min(this.config.maxZoom, this.scale + factor));
 | 
						||
 | 
						||
                if (newScale !== this.scale) {
 | 
						||
                    const worldX = (screenX / this.scale) + this.view.x;
 | 
						||
                    const worldY = (screenY / this.scale) + this.view.y;
 | 
						||
 | 
						||
                    this.scale = newScale;
 | 
						||
 | 
						||
                    this.view.x = worldX - (screenX / this.scale);
 | 
						||
                    this.view.y = worldY - (screenY / this.scale);
 | 
						||
 | 
						||
                    this.draw();
 | 
						||
                    this.debounceSSEUpdate();
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            zoomIn() {
 | 
						||
                this.zoomAt(this.canvas.width / 2, this.canvas.height / 2, this.config.zoomStep);
 | 
						||
            }
 | 
						||
 | 
						||
            zoomOut() {
 | 
						||
                this.zoomAt(this.canvas.width / 2, this.canvas.height / 2, -this.config.zoomStep);
 | 
						||
            }
 | 
						||
 | 
						||
            resetView() {
 | 
						||
                this.view.x = 0;
 | 
						||
                this.view.y = 0;
 | 
						||
                this.scale = 1;
 | 
						||
                this.draw();
 | 
						||
                this.updateSSEConnection();
 | 
						||
            }
 | 
						||
 | 
						||
            clearAll() {
 | 
						||
                if (confirm('Clear all cells? This action cannot be undone.')) {
 | 
						||
                    this.filledCells.clear();
 | 
						||
                    this.draw();
 | 
						||
                    // Note: In a real implementation, you'd want to send this to the server too
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            // Server communication
 | 
						||
            async sendCellState(gridX, gridY, state) {
 | 
						||
                if (!this.csrfToken) {
 | 
						||
                    console.warn('No CSRF token available');
 | 
						||
                    return;
 | 
						||
                }
 | 
						||
 | 
						||
                try {
 | 
						||
                    const response = await fetch('/api/set-pos', {
 | 
						||
                        method: 'POST',
 | 
						||
                        headers: {
 | 
						||
                            'Content-Type': 'application/json',
 | 
						||
                            'X-CSRF-Token': this.csrfToken,
 | 
						||
                        },
 | 
						||
                        body: JSON.stringify({
 | 
						||
                            x: gridX,
 | 
						||
                            y: gridY,
 | 
						||
                            state: state
 | 
						||
                        })
 | 
						||
                    });
 | 
						||
 | 
						||
                    if (!response.ok) {
 | 
						||
                        console.warn(`Failed to send cell state: ${response.status} ${response.statusText}`);
 | 
						||
                    }
 | 
						||
                } catch (error) {
 | 
						||
                    console.error('Error sending cell state to server:', error);
 | 
						||
                    this.updateConnectionStatus('disconnected');
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            async updateSSEConnection() {
 | 
						||
                this.closeSSEConnection();
 | 
						||
 | 
						||
                const bounds = this.getVisibleGridBounds();
 | 
						||
                const params = `${bounds.startX - 1},${bounds.startY - 1},${bounds.endX - bounds.startX + 2},${bounds.endY - bounds.startY + 2}`;
 | 
						||
                const sseUrl = `/sse/${params}`;
 | 
						||
 | 
						||
                try {
 | 
						||
                    this.eventSource = new EventSource(sseUrl);
 | 
						||
                    this.updateConnectionStatus('connecting');
 | 
						||
 | 
						||
                    this.eventSource.addEventListener('state', (event) => {
 | 
						||
                        try {
 | 
						||
                            if (!event.data) {
 | 
						||
                                return;
 | 
						||
                            }
 | 
						||
                            console.log('Received SSE data:', event.data);
 | 
						||
                            const data = new DataView(Uint8Array.from(atob(event.data), char => char.charCodeAt(0)).buffer);
 | 
						||
                            const numIntegers = data.byteLength / 4;
 | 
						||
                            const cellData = [];
 | 
						||
                            for (let i = 0; i < numIntegers - 1; i += 2) {
 | 
						||
                                const pointX = data.getInt32(i * 4, true);
 | 
						||
                                const pointY = data.getInt32((i + 1) * 4, true);
 | 
						||
                                cellData.push([pointX, pointY]);
 | 
						||
                            }
 | 
						||
                            console.log('Received cell data:', cellData);
 | 
						||
 | 
						||
                            this.updateCellsFromServer(cellData);
 | 
						||
                            this.draw();
 | 
						||
                            this.updateConnectionStatus('connected');
 | 
						||
                        } catch (error) {
 | 
						||
                            console.error('Error parsing SSE data:', error);
 | 
						||
                        }
 | 
						||
                    });
 | 
						||
 | 
						||
                    this.eventSource.addEventListener('update', (event) => {
 | 
						||
                        try {
 | 
						||
                            console.log('Received SSE update:', event.data);
 | 
						||
                            const data = new DataView(Uint8Array.from(atob(event.data), char => char.charCodeAt(0)).buffer);
 | 
						||
                            const pointX = data.getInt32(0, true);
 | 
						||
                            const pointY = data.getInt32(4, true);
 | 
						||
                            const state = data.getUint8(8, true);
 | 
						||
                            const key = this.getCellKey(pointX, pointY);
 | 
						||
 | 
						||
                            if (this.filledCells.has(key) && state === 0) {
 | 
						||
                                this.filledCells.delete(key);
 | 
						||
                            } else if (!this.filledCells.has(key) && state === 1) {
 | 
						||
                                this.filledCells.add(key);
 | 
						||
                            }
 | 
						||
 | 
						||
                            this.draw();
 | 
						||
                        } catch (error) {
 | 
						||
                            console.error('Error parsing SSE update:', error);
 | 
						||
                        }
 | 
						||
                    });
 | 
						||
 | 
						||
                    this.eventSource.onerror = async (error) => {
 | 
						||
                        console.error('SSE connection error:', error);
 | 
						||
                        this.updateConnectionStatus('disconnected');
 | 
						||
                        await new Promise(resolve => setTimeout(resolve, 1000));
 | 
						||
                        this.updateSSEConnection();
 | 
						||
                    };
 | 
						||
 | 
						||
                } catch (error) {
 | 
						||
                    console.error('Error establishing SSE connection:', error);
 | 
						||
                    this.updateConnectionStatus('disconnected');
 | 
						||
                    await new Promise(resolve => setTimeout(resolve, 1000));
 | 
						||
                    this.updateSSEConnection();
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            closeSSEConnection() {
 | 
						||
                if (this.eventSource) {
 | 
						||
                    this.eventSource.close();
 | 
						||
                    this.eventSource = null;
 | 
						||
                }
 | 
						||
                clearTimeout(this.sseUpdateTimeout)
 | 
						||
            }
 | 
						||
 | 
						||
            updateCellsFromServer(cellData) {
 | 
						||
                this.filledCells.clear();
 | 
						||
                cellData.forEach(([x, y]) => {
 | 
						||
                    const key = this.getCellKey(x, y);
 | 
						||
                    this.filledCells.add(key);
 | 
						||
                });
 | 
						||
            }
 | 
						||
 | 
						||
            updateConnectionStatus(status) {
 | 
						||
                this.connectionStatus.className = `status-dot status-${status}`;
 | 
						||
 | 
						||
                const statusText = {
 | 
						||
                    connecting: 'Connecting...',
 | 
						||
                    connected: 'Connected',
 | 
						||
                    disconnected: 'Disconnected'
 | 
						||
                };
 | 
						||
 | 
						||
                this.connectionText.textContent = statusText[status] || 'Unknown';
 | 
						||
            }
 | 
						||
 | 
						||
            // Utility methods
 | 
						||
            debounceSSEUpdate() {
 | 
						||
                clearTimeout(this.sseUpdateTimeout);
 | 
						||
                this.sseUpdateTimeout = setTimeout(() => {
 | 
						||
                    this.updateSSEConnection();
 | 
						||
                }, this.config.debounceDelay);
 | 
						||
            }
 | 
						||
 | 
						||
            cleanup() {
 | 
						||
                this.closeSSEConnection();
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        // Initialize the application
 | 
						||
        const gridCanvas = new GridCanvas('canvas');
 | 
						||
    </script>
 | 
						||
</body>
 | 
						||
 | 
						||
</html> |