Reference implementation for the Phoenix Architecture. Work in progress.
aicoding.leaflet.pub/
ai
coding
crazy
1export interface ViewportInfo {
2 width: number;
3 height: number;
4 devicePixelRatio: number;
5 isMobile: boolean;
6}
7
8export interface TouchPoint {
9 x: number;
10 y: number;
11 identifier: number;
12}
13
14export interface PointerEvent {
15 x: number;
16 y: number;
17 type: 'start' | 'move' | 'end';
18 pressure?: number;
19}
20
21export type PointerEventHandler = (event: PointerEvent) => void;
22
23export class ResponsiveCanvas {
24 private canvasId: string;
25 private containerId: string;
26 private scale: number = 1;
27 private offsetX: number = 0;
28 private offsetY: number = 0;
29 private pointerHandlers: Set<PointerEventHandler> = new Set();
30 private activeTouches: Map<number, TouchPoint> = new Map();
31 private canvasWidth: number = 800;
32 private canvasHeight: number = 600;
33 private containerWidth: number = 800;
34 private containerHeight: number = 600;
35
36 constructor(canvasId: string, containerId: string) {
37 this.canvasId = canvasId;
38 this.containerId = containerId;
39 this.updateScale();
40 }
41
42 private updateScale(): void {
43 const scaleX = this.containerWidth / this.canvasWidth;
44 const scaleY = this.containerHeight / this.canvasHeight;
45 this.scale = Math.min(scaleX, scaleY, 1);
46
47 const scaledWidth = this.canvasWidth * this.scale;
48 const scaledHeight = this.canvasHeight * this.scale;
49 this.offsetX = (this.containerWidth - scaledWidth) / 2;
50 this.offsetY = (this.containerHeight - scaledHeight) / 2;
51 }
52
53 private getCanvasCoordinates(clientX: number, clientY: number): { x: number; y: number } {
54 const x = (clientX - this.offsetX) / this.scale;
55 const y = (clientY - this.offsetY) / this.scale;
56 return { x, y };
57 }
58
59 public handlePointerStart(clientX: number, clientY: number, pressure: number = 1, identifier?: number): void {
60 const coords = this.getCanvasCoordinates(clientX, clientY);
61
62 if (identifier !== undefined) {
63 this.activeTouches.set(identifier, {
64 x: coords.x,
65 y: coords.y,
66 identifier
67 });
68 }
69
70 this.emitPointerEvent({
71 x: coords.x,
72 y: coords.y,
73 type: 'start',
74 pressure
75 });
76 }
77
78 public handlePointerMove(clientX: number, clientY: number, pressure: number = 1, identifier?: number): void {
79 const coords = this.getCanvasCoordinates(clientX, clientY);
80
81 if (identifier !== undefined) {
82 if (this.activeTouches.has(identifier)) {
83 this.activeTouches.set(identifier, {
84 x: coords.x,
85 y: coords.y,
86 identifier
87 });
88 } else {
89 return;
90 }
91 }
92
93 this.emitPointerEvent({
94 x: coords.x,
95 y: coords.y,
96 type: 'move',
97 pressure
98 });
99 }
100
101 public handlePointerEnd(clientX: number, clientY: number, identifier?: number): void {
102 const coords = this.getCanvasCoordinates(clientX, clientY);
103
104 if (identifier !== undefined) {
105 if (this.activeTouches.has(identifier)) {
106 this.activeTouches.delete(identifier);
107 } else {
108 return;
109 }
110 }
111
112 this.emitPointerEvent({
113 x: coords.x,
114 y: coords.y,
115 type: 'end',
116 pressure: 0
117 });
118 }
119
120 private emitPointerEvent(event: PointerEvent): void {
121 this.pointerHandlers.forEach(handler => {
122 try {
123 handler(event);
124 } catch (error) {
125 console.error('Error in pointer event handler:', error);
126 }
127 });
128 }
129
130 public addPointerEventListener(handler: PointerEventHandler): void {
131 this.pointerHandlers.add(handler);
132 }
133
134 public removePointerEventListener(handler: PointerEventHandler): void {
135 this.pointerHandlers.delete(handler);
136 }
137
138 public setCanvasSize(width: number, height: number): void {
139 this.canvasWidth = width;
140 this.canvasHeight = height;
141 this.updateScale();
142 }
143
144 public setContainerSize(width: number, height: number): void {
145 this.containerWidth = width;
146 this.containerHeight = height;
147 this.updateScale();
148 }
149
150 public getViewportInfo(): ViewportInfo {
151 return {
152 width: this.containerWidth,
153 height: this.containerHeight,
154 devicePixelRatio: 1,
155 isMobile: this.detectMobile()
156 };
157 }
158
159 private detectMobile(): boolean {
160 return false;
161 }
162
163 public getScale(): number {
164 return this.scale;
165 }
166
167 public getCanvasHTML(): string {
168 return `<canvas id="${this.canvasId}" width="${this.canvasWidth}" height="${this.canvasHeight}" style="transform: scale(${this.scale}); transform-origin: top left; position: absolute; left: ${this.offsetX}px; top: ${this.offsetY}px;"></canvas>`;
169 }
170
171 public destroy(): void {
172 this.pointerHandlers.clear();
173 this.activeTouches.clear();
174 }
175}
176
177export function createResponsiveCanvas(canvasId: string, containerId: string): ResponsiveCanvas {
178 return new ResponsiveCanvas(canvasId, containerId);
179}
180
181export function isMobileDevice(): boolean {
182 return false;
183}
184
185/** @internal Phoenix VCS traceability — do not remove. */
186export const _phoenix = {
187 iu_id: '34e0a94555fd05dabb716eb8d9f35d4ad180e267ff13ca20cc74e1b3326659db',
188 name: 'Responsiveness',
189 risk_tier: 'low',
190 canon_ids: [2 as const],
191} as const;