export class GyroController { private enabled = false; private pending = false; private generation = 0; private hasReading = false; private base: { beta: number; gamma: number } | null = null; constructor(private readonly setView: (x: number, y: number) => void, private readonly status: (message: string) => void) {} async toggle(): Promise { if (this.enabled || this.pending) { this.dispose(); return false; } if (!window.isSecureContext || !('DeviceOrientationEvent' in window)) { this.status('자이로스코프를 사용할 수 없습니다.'); return false; } const generation = ++this.generation; this.pending = true; try { const device = window.DeviceOrientationEvent as typeof DeviceOrientationEvent & { requestPermission?: () => Promise }; this.status('센서 권한을 기다리는 중…'); if (device.requestPermission && await device.requestPermission() !== 'granted') throw new Error('권한 거절됨'); if (generation !== this.generation) return false; this.base = null; this.hasReading = false; this.pending = false; this.enabled = true; window.addEventListener('deviceorientation', this.onOrientation); this.status('센서값을 기다리는 중…'); return true; } catch (error) { this.pending = false; if (generation === this.generation) this.status(`자이로스코프를 사용할 수 없습니다 (${(error as Error).message})`); return false; } } reset(): void { this.base = null; } dispose(): void { ++this.generation; this.pending = false; this.enabled = false; this.base = null; window.removeEventListener('deviceorientation', this.onOrientation); } private onOrientation = (event: DeviceOrientationEvent): void => { if (!this.enabled || event.beta == null || event.gamma == null) return; if (!this.base) this.base = { beta: event.beta, gamma: event.gamma }; const angle = (screen.orientation?.angle ?? 0) * Math.PI / 180; const x = event.gamma - this.base.gamma; const y = event.beta - this.base.beta; this.setView((x * Math.cos(angle) + y * Math.sin(angle)) / 35, (y * Math.cos(angle) - x * Math.sin(angle)) / 35); if (!this.hasReading) { this.hasReading = true; this.status('자이로스코프 활성'); } }; }