feat(web): proper viewport scaling, instant load, and file downloads

Make the Emscripten build behave like the desktop app for loading, window
scaling, and exporting. All changes are #ifdef __EMSCRIPTEN__-gated or no-op on
desktop, so native behavior is unchanged.

Scaling:
- Drop FLAG_WINDOW_HIGHDPI on web. The emscripten-GLFW shim forces a fixed
  pixel canvas style (!important) when HiDPI-aware, overriding the shell's
  100vw/100vh CSS so the canvas can't fill the page; raylib's own resize/window
  callbacks also disagree about dividing by devicePixelRatio, desyncing the
  framebuffer from the reported screen size.
- Sync raylib's window size to window.innerWidth/innerHeight each frame via
  SetWindowSize (guarded against no-op churn). This keeps screen size, GL
  viewport, and projection consistent, so the UI fills the viewport and reflows
  on resize like the desktop window.

Loading:
- Compute the full-resolution STFT synchronously when a file loads instead of
  the desktop overview-then-deferred-high-res path, which relied on many
  main-loop iterations yielding to the browser and appeared to stall partway.
- Allow the wasm heap to grow (INITIAL_MEMORY + ALLOW_MEMORY_GROWTH) so longer
  recordings fit now that everything is computed up front.

Exports:
- Add Platform_OfferFileToUser(): no-op on desktop (file is already on disk);
  on web it reads the just-written file from MEMFS and triggers a browser
  download, then unlinks the temp copy. Wired into PNG and WAV export, which
  now show just the filename in the status message.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 11:48:47 -07:00
parent 6347eb172e
commit cef7619833
8 changed files with 118 additions and 6 deletions
+55
View File
@@ -20,6 +20,23 @@
#include <stdbool.h>
#include <stdio.h>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
// Keep raylib's framebuffer/screen size matched to the browser viewport, so the
// (immediate-mode) UI fills the page and reflows on window resize the same way
// the desktop OS window does. Going through SetWindowSize keeps the screen size,
// GL viewport, and projection consistent; the != guard avoids per-frame churn.
static void SyncCanvasToWindow(void)
{
int w = EM_ASM_INT({ return window.innerWidth; });
int h = EM_ASM_INT({ return window.innerHeight; });
if (w > 0 && h > 0 && (w != GetScreenWidth() || h != GetScreenHeight())) {
SetWindowSize(w, h);
}
}
#endif
// ============================================================================
// Global State (declared extern in spectrogram_types.h)
// ============================================================================
@@ -217,7 +234,18 @@ static void DispatchKeymap(void)
int main(int argc, char* argv[])
{
#ifdef __EMSCRIPTEN__
// FLAG_WINDOW_HIGHDPI is buggy on the web backend: the Emscripten resize
// callback sets the screen size to window.innerWidth, but the GLFW window-
// size callback it triggers divides that by devicePixelRatio when HIGHDPI
// is set. On a HiDPI display the framebuffer and the reported screen size
// desync and the UI renders into a corner. UI scaling is handled by
// GetUIScale() regardless, so the flag is unnecessary here. raylib auto-
// resizes the canvas to the window when FLAG_WINDOW_RESIZABLE is set.
SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE);
#else
SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI);
#endif
InitWindow(1280, 800, "Spectrogram Viewer");
SetTargetFPS(60);
SetTraceLogLevel(LOG_WARNING); // Suppress INFO texture logs
@@ -320,6 +348,11 @@ int main(int argc, char* argv[])
while (!WindowShouldClose())
{
#ifdef __EMSCRIPTEN__
// Track the browser viewport (fill + reflow on resize, like desktop).
SyncCanvasToWindow();
#endif
// Drag & Drop
if (IsFileDropped()) {
FilePathList dropped = LoadDroppedFiles();
@@ -764,6 +797,27 @@ int main(int argc, char* argv[])
// Processing (incremental across frames)
if (app.loaded && !app.stftComputed) {
#ifdef __EMSCRIPTEN__
// Web build: there are no worker threads, and the desktop path's
// overview-then-deferred-high-res fill depends on many main-loop
// iterations yielding to the browser (which made loading appear to
// stall partway). Compute the full-resolution STFT in one shot so
// the spectrogram is completely ready as soon as the file loads.
ComputeSTFTInit(&app.signal, &app.stft, app.fftSize);
app.skipFactor = 1; // full resolution, no overview stride
ComputeSTFTIncremental(&app.signal, &app.stft, app.fftSize, 0); // computes every segment
AutoScaleAmplitude(&app.stft);
GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
app.currentSTFTSegment = app.stft.numSegments;
app.bgHighResSeg = app.stft.numSegments;
app.loadingProgress = 1.0f;
app.stftComputed = true;
app.highResFinished = true;
app.bgFinished = true;
app.isBgProcessing = false;
app.loadingPhase = 0;
SaveToCache();
#else
if (app.loadingPhase == 0) {
// Initialize STFT once
ComputeSTFTInit(&app.signal, &app.stft, app.fftSize);
@@ -802,6 +856,7 @@ int main(int argc, char* argv[])
// Save the overview result to cache (will be overwritten when full-res completes)
SaveToCache();
}
#endif // __EMSCRIPTEN__
}
// Loading overlay (drawn during STFT computation)