Add vpinball: a browser Visual Pinball player

Next.js app that plays real .vpx tables via @valknar/vpinball-wasm
(WebAssembly Visual Pinball). Demo mode with the bundled default
table, drag-and-drop custom table upload, a cabinet-styled UI with a
coin-door HUD (fullscreen/stats/info/eject), a custom 404 page, PWA
support (hand-rolled service worker), and a static Docker Compose
deployment behind nginx.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hQoM3jJT1Lx7CMTciMzvD
This commit is contained in:
2026-08-23 05:25:15 +02:00
co-authored by Claude Sonnet 5
commit e493a12e68
51 changed files with 5570 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
"use client";
import { useEffect } from "react";
import type { attachTouchControls as AttachTouchControls } from "@valknar/vpinball-wasm";
const ENGINE_BASE_URL = "/vendor/vpinball-wasm";
function isTouchDevice(): boolean {
return typeof window !== "undefined" && ("ontouchstart" in window || navigator.maxTouchPoints > 0);
}
/** Attaches the engine's on-screen touch overlay to `containerRef` while `active` is true. */
export function useTouchControls(
containerRef: React.RefObject<HTMLElement | null>,
active: boolean,
): void {
useEffect(() => {
if (!active || !isTouchDevice()) return;
const container = containerRef.current;
if (!container) return;
let handle: { detach: () => void } | undefined;
let cancelled = false;
(async () => {
const { attachTouchControls } = (await import(
/* webpackIgnore: true */ `${ENGINE_BASE_URL}/index.js`
)) as { attachTouchControls: typeof AttachTouchControls };
if (cancelled) return;
handle = attachTouchControls({ container });
})();
return () => {
cancelled = true;
handle?.detach();
};
}, [containerRef, active]);
}