Add boot loading placeholder to cover cold-load window, lazy-load smplr off the critical path
This commit is contained in:
+54
-1
@@ -15,9 +15,62 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
<script>
|
||||||
|
// Anti-flash + boot theme: apply the persisted/system theme to <html>
|
||||||
|
// BEFORE first paint, mirroring getInitialTheme() in the app exactly
|
||||||
|
// (localStorage "b2b-theme" -> prefers-color-scheme -> light fallback).
|
||||||
|
// React sets the same value on mount, so there's no conflict - this just
|
||||||
|
// makes the cold-load window (before the JS bundle parses) show the right
|
||||||
|
// background instead of a white flash. Wrapped in try/catch so a blocked
|
||||||
|
// localStorage (private mode) can never break boot.
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var t = localStorage.getItem("b2b-theme");
|
||||||
|
if (t !== "dark" && t !== "light") {
|
||||||
|
t = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
document.documentElement.dataset.theme = t;
|
||||||
|
} catch (e) {
|
||||||
|
document.documentElement.dataset.theme = "light";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Boot placeholder shown during the cold-load window (bundle download +
|
||||||
|
parse + service-worker install) before React mounts. Lives inside #root
|
||||||
|
so React's createRoot().render() replaces it automatically on first
|
||||||
|
render - no cleanup code needed. Inline so it works before the CSS
|
||||||
|
chunk loads. Colors mirror --color-bg (dark #1a1a2e / light #ffffff). */
|
||||||
|
html { background: #1a1a2e; }
|
||||||
|
html[data-theme="light"] { background: #ffffff; }
|
||||||
|
#b2b-boot {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #1a1a2e;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] #b2b-boot { background: #ffffff; }
|
||||||
|
#b2b-boot .b2b-spinner {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid rgba(147, 51, 234, 0.25);
|
||||||
|
border-top-color: #9333ea;
|
||||||
|
animation: b2b-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes b2b-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root">
|
||||||
|
<div id="b2b-boot"><div class="b2b-spinner"></div></div>
|
||||||
|
</div>
|
||||||
<!-- Error catcher: shows any unhandled JS error directly on screen -->
|
<!-- Error catcher: shows any unhandled JS error directly on screen -->
|
||||||
<div id="error-overlay" style="display:none;position:fixed;inset:0;background:#1a0000;color:#ff6b6b;padding:24px;font:14px monospace;z-index:9999;white-space:pre-wrap;overflow:auto;"></div>
|
<div id="error-overlay" style="display:none;position:fixed;inset:0;background:#1a0000;color:#ff6b6b;padding:24px;font:14px monospace;z-index:9999;white-space:pre-wrap;overflow:auto;"></div>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { SplendidGrandPiano, Soundfont, ElectricPiano } from "smplr";
|
|
||||||
import type { NoteEvent } from "@bach-to-basics/shared";
|
import type { NoteEvent } from "@bach-to-basics/shared";
|
||||||
|
|
||||||
// ── Instrument catalogue ──────────────────────────────────────────────────────
|
// ── Instrument catalogue ──────────────────────────────────────────────────────
|
||||||
@@ -36,15 +35,19 @@ export type Player = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a smplr player for the given instrument against any AudioContext-like.
|
* Build a smplr player for the given instrument against any AudioContext-like.
|
||||||
* Exported (not local) so the MP3 export pipeline can reuse it for an
|
*
|
||||||
* OfflineAudioContext - same sample set / same instrument as live playback,
|
* smplr is imported DYNAMICALLY here (not at module top level) so it lands in
|
||||||
* which is critical for the rendered audio to match what the user hears.
|
* its own lazy chunk instead of the main bundle. The piano keyboard (PixiJS),
|
||||||
|
* falling notes, and sheet music don't need smplr to render, so keeping it out
|
||||||
|
* of the critical path lets the first paint happen much sooner. smplr only
|
||||||
|
* loads on the first audio interaction (pre-warm on mount, or first note).
|
||||||
*
|
*
|
||||||
* Accepts `BaseAudioContext` (the common parent of both AudioContext and
|
* Accepts `BaseAudioContext` (the common parent of both AudioContext and
|
||||||
* OfflineAudioContext) rather than `AudioContext` alone, so callers can pass
|
* OfflineAudioContext) so callers can pass an offline context for batch
|
||||||
* an offline context for batch rendering. The smplr classes accept either.
|
* rendering. The smplr classes accept either.
|
||||||
*/
|
*/
|
||||||
export function createPlayer(id: InstrumentId, ctx: BaseAudioContext): Player {
|
export async function createPlayer(id: InstrumentId, ctx: BaseAudioContext): Promise<Player> {
|
||||||
|
const { SplendidGrandPiano, Soundfont, ElectricPiano } = await import("smplr");
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case "grand":
|
case "grand":
|
||||||
return new SplendidGrandPiano(ctx as AudioContext) as unknown as Player;
|
return new SplendidGrandPiano(ctx as AudioContext) as unknown as Player;
|
||||||
@@ -79,7 +82,7 @@ export class AudioEngine {
|
|||||||
this._loadPromise = (async () => {
|
this._loadPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
if (!this.ctx) this.ctx = new AudioContext();
|
if (!this.ctx) this.ctx = new AudioContext();
|
||||||
this.player = createPlayer(this._currentId, this.ctx);
|
this.player = await createPlayer(this._currentId, this.ctx);
|
||||||
await this.player.load;
|
await this.player.load;
|
||||||
this._loaded = true;
|
this._loaded = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user