fuzzyworld

bubbles / files / src / main.ts

main.ts

5.7 KB · history · blame

1import { App } from './App.js';
2import { WaterLevel } from './levels/WaterLevel.js';
3import { LavaLampLevel } from './levels/LavaLampLevel.js';
4import { ShampooLevel } from './levels/ShampooLevel.js';
5import { SandWaterLevel } from './levels/SandWaterLevel.js';
6import QRCode from 'qrcode';
7
8const canvas = document.getElementById('canvas') as HTMLCanvasElement | null;
9const startScreen = document.getElementById('start-screen') as HTMLDivElement;
10const startBtn = document.getElementById('start-btn') as HTMLButtonElement;
11if (!canvas) throw new Error('No canvas element found');
12
13const isIOS = /iP(hone|od|ad)/.test(navigator.userAgent);
14const supportsFullscreen = document.fullscreenEnabled
15 // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 || (document as any).webkitFullscreenEnabled;
17
18let orientationLocked = false;
19
20async function tryLockOrientation(): Promise<void> {
21 try {
22 // eslint-disable-next-line @typescript-eslint/no-explicit-any
23 await (screen.orientation as any).lock('portrait');
24 orientationLocked = true;
25 } catch { /* not supported — CSS landscape overlay handles it */ }
26}
27
28async function enterFullscreen(): Promise<void> {
29 if (!isIOS && supportsFullscreen) {
30 try {
31 await document.documentElement.requestFullscreen({ navigationUI: 'hide' });
32 } catch { /* ignore */ }
33 }
34 await tryLockOrientation();
35}
36
37document.addEventListener('fullscreenchange', () => {
38 if (document.fullscreenElement && !orientationLocked) {
39 void tryLockOrientation();
40 }
41});
42
43screen.orientation.addEventListener('change', () => {
44 if (orientationLocked) void tryLockOrientation();
45});
46
47// ── App + levels ──────────────────────────────────────────────────────────
48
49const app = new App(canvas);
50
51// Build all levels sharing the same ctx/glass geometry from app
52// (App exposes canvas; levels read glass dims on construction via glassX/Y/W/H)
53const ctx = app.canvas.getContext('2d')!;
54
55function makeAllLevels(): void {
56 const W = window.innerWidth;
57 const H = window.innerHeight;
58 const glassW = Math.min(W * 0.82, 420);
59 const glassH = H * 0.72;
60 const glassX = (W - glassW) / 2;
61 const glassY = (H - glassH) / 2 - H * 0.02;
62
63 app.setLevels([
64 new WaterLevel(ctx, glassX, glassY, glassW, glassH),
65 new LavaLampLevel(ctx, glassX, glassY, glassW, glassH),
66 new ShampooLevel(ctx, glassX, glassY, glassW, glassH),
67 new SandWaterLevel(ctx, glassX, glassY, glassW, glassH),
68 ]);
69}
70makeAllLevels();
71
72// ── QR body ───────────────────────────────────────────────────────────────
73
74void (async () => {
75 const qrCanvas = document.createElement('canvas');
76 await QRCode.toCanvas(qrCanvas, window.location.href, {
77 width: 200,
78 margin: 1,
79 color: { dark: '#000000', light: '#ffffff' },
80 });
81 app.spawnQRBody(qrCanvas);
82})();
83
84// ── Reset button ──────────────────────────────────────────────────────────
85
86const resetBtn = document.getElementById('reset-btn') as HTMLButtonElement;
87resetBtn.addEventListener('click', () => app.reset());
88
89// ── Level dot indicator ───────────────────────────────────────────────────
90
91const dotsEl = document.getElementById('level-dots') as HTMLDivElement;
92let dotsTimeout = 0;
93
94function updateDots(index: number, total: number): void {
95 dotsEl.innerHTML = '';
96 for (let i = 0; i < total; i++) {
97 const dot = document.createElement('span');
98 dot.className = 'dot' + (i === index ? ' active' : '');
99 dotsEl.appendChild(dot);
100 }
101 dotsEl.classList.add('visible');
102 clearTimeout(dotsTimeout);
103 dotsTimeout = window.setTimeout(() => dotsEl.classList.remove('visible'), 2000);
104}
105
106app.onLevelChange = (index, total) => updateDots(index, total);
107
108// ── Touch: tap to drop, swipe to switch level ─────────────────────────────
109
110let pointerStartX = 0;
111let pointerStartY = 0;
112let pointerMoved = false;
113
114canvas.addEventListener('pointerdown', async (e: PointerEvent) => {
115 pointerStartX = e.clientX;
116 pointerStartY = e.clientY;
117 pointerMoved = false;
118 await app.requestPermissionOnce();
119});
120
121canvas.addEventListener('pointermove', (e: PointerEvent) => {
122 if (Math.abs(e.clientX - pointerStartX) > 8 || Math.abs(e.clientY - pointerStartY) > 8) {
123 pointerMoved = true;
124 }
125});
126
127canvas.addEventListener('pointerup', (e: PointerEvent) => {
128 const dx = e.clientX - pointerStartX;
129 const dy = e.clientY - pointerStartY;
130 const dist = Math.sqrt(dx * dx + dy * dy);
131
132 if (dist > 40 && Math.abs(dx) > Math.abs(dy) * 1.5) {
133 // Horizontal swipe
134 if (dx < 0) app.nextLevel(); else app.prevLevel();
135 updateDots(app.levelIndex, app.levelCount);
136 } else if (!pointerMoved || dist < 12) {
137 // Tap
138 app.touch(e.clientX, e.clientY);
139 }
140 pointerMoved = false;
141});
142
143// ── Start button ──────────────────────────────────────────────────────────
144
145startBtn.addEventListener('click', async () => {
146 await enterFullscreen();
147 startScreen.classList.add('hidden');
148 updateDots(app.levelIndex, app.levelCount);
149 app.start();
150}, { once: true });
151
152