Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 250 lines 7.4 kB view raw
1export interface GridCell { 2 x: number; 3 y: number; 4 ownerId: string | null; 5 teamColor: string | null; 6} 7 8export interface GridState { 9 cells: Map<string, GridCell>; 10 gridSize: number; 11 cellSize: number; 12} 13 14export interface GridRenderOptions { 15 gridSize: number; 16 onCellClick: (x: number, y: number) => void; 17} 18 19export interface CanvasLike { 20 width: number; 21 height: number; 22 style: { cursor: string }; 23 addEventListener(type: string, listener: (event: any) => void): void; 24 removeEventListener(type: string, listener: (event: any) => void): void; 25 getBoundingClientRect(): { left: number; top: number }; 26 getContext(type: string): CanvasRenderingContext2DLike | null; 27} 28 29export interface CanvasRenderingContext2DLike { 30 clearRect(x: number, y: number, width: number, height: number): void; 31 fillRect(x: number, y: number, width: number, height: number): void; 32 beginPath(): void; 33 moveTo(x: number, y: number): void; 34 lineTo(x: number, y: number): void; 35 stroke(): void; 36 save(): void; 37 restore(): void; 38 fillStyle: string; 39 strokeStyle: string; 40 lineWidth: number; 41 shadowColor: string; 42 shadowBlur: number; 43} 44 45export class GridRenderer { 46 private canvas: CanvasLike | null = null; 47 private ctx: CanvasRenderingContext2DLike | null = null; 48 private gridSize: number; 49 private cellSize: number; 50 private state: GridState; 51 private hoveredCell: { x: number; y: number } | null = null; 52 private onCellClick: (x: number, y: number) => void; 53 private mouseMoveHandler: (event: any) => void; 54 private mouseLeaveHandler: () => void; 55 private clickHandler: (event: any) => void; 56 57 constructor(options: GridRenderOptions) { 58 this.gridSize = options.gridSize; 59 this.cellSize = 500 / this.gridSize; 60 this.onCellClick = options.onCellClick; 61 62 this.state = { 63 cells: new Map(), 64 gridSize: this.gridSize, 65 cellSize: this.cellSize 66 }; 67 68 this.mouseMoveHandler = (event: any) => { 69 if (!this.canvas) return; 70 const rect = this.canvas.getBoundingClientRect(); 71 const x = Math.floor((event.clientX - rect.left) / this.cellSize); 72 const y = Math.floor((event.clientY - rect.top) / this.cellSize); 73 74 if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { 75 if (!this.hoveredCell || this.hoveredCell.x !== x || this.hoveredCell.y !== y) { 76 this.hoveredCell = { x, y }; 77 this.render(); 78 } 79 } else { 80 if (this.hoveredCell) { 81 this.hoveredCell = null; 82 this.render(); 83 } 84 } 85 }; 86 87 this.mouseLeaveHandler = () => { 88 if (this.hoveredCell) { 89 this.hoveredCell = null; 90 this.render(); 91 } 92 }; 93 94 this.clickHandler = (event: any) => { 95 if (!this.canvas) return; 96 const rect = this.canvas.getBoundingClientRect(); 97 const x = Math.floor((event.clientX - rect.left) / this.cellSize); 98 const y = Math.floor((event.clientY - rect.top) / this.cellSize); 99 100 if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { 101 this.onCellClick(x, y); 102 } 103 }; 104 } 105 106 public setCanvas(canvas: CanvasLike): void { 107 this.canvas = canvas; 108 const ctx = this.canvas.getContext('2d'); 109 if (!ctx) { 110 throw new Error('Failed to get 2D rendering context'); 111 } 112 this.ctx = ctx; 113 this.setupCanvas(); 114 this.attachEventListeners(); 115 } 116 117 private setupCanvas(): void { 118 if (!this.canvas) return; 119 this.canvas.width = 500; 120 this.canvas.height = 500; 121 this.canvas.style.cursor = 'pointer'; 122 } 123 124 private attachEventListeners(): void { 125 if (!this.canvas) return; 126 this.canvas.addEventListener('mousemove', this.mouseMoveHandler); 127 this.canvas.addEventListener('mouseleave', this.mouseLeaveHandler); 128 this.canvas.addEventListener('click', this.clickHandler); 129 } 130 131 public updateCell(x: number, y: number, ownerId: string | null, teamColor: string | null): void { 132 const key = `${x},${y}`; 133 this.state.cells.set(key, { x, y, ownerId, teamColor }); 134 this.render(); 135 } 136 137 public updateGrid(cells: GridCell[]): void { 138 this.state.cells.clear(); 139 for (const cell of cells) { 140 const key = `${cell.x},${cell.y}`; 141 this.state.cells.set(key, cell); 142 } 143 this.render(); 144 } 145 146 private render(): void { 147 if (!this.ctx) return; 148 this.ctx.clearRect(0, 0, 500, 500); 149 150 // Render all cells 151 for (let x = 0; x < this.gridSize; x++) { 152 for (let y = 0; y < this.gridSize; y++) { 153 this.renderCell(x, y); 154 } 155 } 156 157 // Render grid lines 158 this.renderGridLines(); 159 } 160 161 private renderCell(x: number, y: number): void { 162 if (!this.ctx) return; 163 const key = `${x},${y}`; 164 const cell = this.state.cells.get(key); 165 const pixelX = x * this.cellSize; 166 const pixelY = y * this.cellSize; 167 168 // Base cell color 169 if (cell && cell.ownerId && cell.teamColor) { 170 // Owned cell with team color and glow effect 171 this.ctx.fillStyle = cell.teamColor; 172 this.ctx.fillRect(pixelX, pixelY, this.cellSize, this.cellSize); 173 174 // Add glow effect 175 this.ctx.save(); 176 this.ctx.shadowColor = cell.teamColor; 177 this.ctx.shadowBlur = 8; 178 this.ctx.fillStyle = cell.teamColor; 179 this.ctx.fillRect(pixelX + 2, pixelY + 2, this.cellSize - 4, this.cellSize - 4); 180 this.ctx.restore(); 181 } else { 182 // Empty cell - dark gray 183 this.ctx.fillStyle = '#2a2a3e'; 184 this.ctx.fillRect(pixelX, pixelY, this.cellSize, this.cellSize); 185 } 186 187 // Hover highlight 188 if (this.hoveredCell && this.hoveredCell.x === x && this.hoveredCell.y === y) { 189 this.ctx.save(); 190 this.ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; 191 this.ctx.fillRect(pixelX, pixelY, this.cellSize, this.cellSize); 192 this.ctx.restore(); 193 } 194 } 195 196 private renderGridLines(): void { 197 if (!this.ctx) return; 198 this.ctx.strokeStyle = '#1a1a2e'; 199 this.ctx.lineWidth = 1; 200 201 // Vertical lines 202 for (let x = 0; x <= this.gridSize; x++) { 203 const pixelX = x * this.cellSize; 204 this.ctx.beginPath(); 205 this.ctx.moveTo(pixelX, 0); 206 this.ctx.lineTo(pixelX, 500); 207 this.ctx.stroke(); 208 } 209 210 // Horizontal lines 211 for (let y = 0; y <= this.gridSize; y++) { 212 const pixelY = y * this.cellSize; 213 this.ctx.beginPath(); 214 this.ctx.moveTo(0, pixelY); 215 this.ctx.lineTo(500, pixelY); 216 this.ctx.stroke(); 217 } 218 } 219 220 public getCanvas(): CanvasLike | null { 221 return this.canvas; 222 } 223 224 public destroy(): void { 225 if (this.canvas) { 226 this.canvas.removeEventListener('mousemove', this.mouseMoveHandler); 227 this.canvas.removeEventListener('mouseleave', this.mouseLeaveHandler); 228 this.canvas.removeEventListener('click', this.clickHandler); 229 } 230 } 231} 232 233export function createGridRenderer(gridSize: number, onCellClick: (x: number, y: number) => void): GridRenderer { 234 return new GridRenderer({ 235 gridSize, 236 onCellClick 237 }); 238} 239 240export function generateGridHTML(containerId: string): string { 241 return `<div id="${containerId}" style="display: flex; justify-content: center; align-items: center; width: 100%; height: 100%;"><canvas width="500" height="500" style="cursor: pointer;"></canvas></div>`; 242} 243 244/** @internal Phoenix VCS traceability — do not remove. */ 245export const _phoenix = { 246 iu_id: 'd219e8bbb48e26fb3e3dad39edc3d9dd63fcdac50788f17f8224cb924096e840', 247 name: 'Grid Rendering', 248 risk_tier: 'medium', 249 canon_ids: [6 as const], 250} as const;