Reference implementation for the Phoenix Architecture. Work in progress.
aicoding.leaflet.pub/
ai
coding
crazy
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;