From 53c8ea03ac480868c806b1ca7b0b4fc55c834efc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?=
Date: Mon, 24 Aug 2026 09:52:30 +0200
Subject: [PATCH] Show a spinner while the table is starting
instance.start() runs synchronously and blocks the main thread for a
noticeable moment on heavy WASM init, so pressing "Tap to Start" would
freeze on the old screen with no feedback until the HUD suddenly
appeared. Add a "starting" status between "ready" and "running", and
defer the actual start() call by a nested rAF so the browser gets a
chance to paint the spinner before the blocking call runs.
Co-Authored-By: Claude Sonnet 5
---
components/pinball/PinballCanvasImpl.tsx | 7 +++++++
lib/pinball/usePinballInstance.ts | 16 +++++++++++++---
2 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/components/pinball/PinballCanvasImpl.tsx b/components/pinball/PinballCanvasImpl.tsx
index e1229bb..8686ef5 100644
--- a/components/pinball/PinballCanvasImpl.tsx
+++ b/components/pinball/PinballCanvasImpl.tsx
@@ -74,6 +74,13 @@ export default function PinballCanvasImpl({ tableData, rom, tableInfo }: Pinball
)}
+
+ {status === "starting" && (
+
+ )}
);
}
diff --git a/lib/pinball/usePinballInstance.ts b/lib/pinball/usePinballInstance.ts
index 352b88c..d5eee96 100644
--- a/lib/pinball/usePinballInstance.ts
+++ b/lib/pinball/usePinballInstance.ts
@@ -10,7 +10,7 @@ import { hasWebGL2 } from "./webglSupport";
// outside webpack's module graph (see scripts/copy-engine-assets.mjs).
const ENGINE_BASE_URL = "/vendor/vpinball-wasm";
-export type PinballStatus = "idle" | "loading" | "ready" | "running" | "error";
+export type PinballStatus = "idle" | "loading" | "ready" | "starting" | "running" | "error";
interface EngineModule {
loadPinball(options: LoadPinballOptions): Promise;
@@ -114,8 +114,18 @@ export function usePinballInstance(
const start = useCallback(() => {
if (!instanceRef.current) return;
- instanceRef.current.start();
- setStatus("running");
+ setStatus("starting");
+ // instance.start() runs synchronously and blocks the main thread for a
+ // noticeable moment (heavy WASM init) — a nested rAF lets the browser
+ // paint the "starting" spinner from the state update above before that
+ // block hits, instead of the UI freezing on the old screen mid-click.
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ if (disposedRef.current || !instanceRef.current) return;
+ instanceRef.current.start();
+ setStatus("running");
+ });
+ });
}, []);
return { status, progress, error, loadTimeMs, instanceRef, start };