diff --git a/README.md b/README.md index 8336b7b..a67a7ad 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This C++ library provides base classes for creating 2D virtual worlds. *A larger demo world (`--world meadow`): sheep graze and wander, wolves stalk the nearest sheep, flowers stay put. The grass under each location is the demo's own state, shown as the floor colour.* -The frontend reads an `Environment` and draws it with the [Strata](https://github.com/Preponderous-Software/strata) engine (v0.6.0). It never changes the world: CI runs each demo with and without drawing and compares the results. A consumer writes one callback that turns an `Entity` into a shape, colour and size; everything else (floor, camera, tweening, picking a location with the mouse, a HUD) is provided. It is opt-in and built separately with CMake, so the library above still needs nothing but a C++ compiler. See [docs/STRATA.md](docs/STRATA.md). +The frontend reads an `Environment` and draws it with the [Strata](https://github.com/Preponderous-Software/strata) engine (v0.7.0). It never changes the world: CI runs each demo with and without drawing and compares the results. A consumer writes one callback that turns an `Entity` into a shape, colour and size; everything else (floor, camera, tweening, picking a location with the mouse, a HUD) is provided. It is opt-in and built separately with CMake, so the library above still needs nothing but a C++ compiler. See [docs/STRATA.md](docs/STRATA.md). ``` cmake -S strata -B build-strata -DCMAKE_BUILD_TYPE=Release diff --git a/docs/STRATA.md b/docs/STRATA.md index 1ca4543..ea7e933 100644 --- a/docs/STRATA.md +++ b/docs/STRATA.md @@ -14,7 +14,7 @@ Nothing in the library depends on it. `make`, `run_tests.sh` and the top-level CMake 3.24 or newer (Strata's minimum), a C++17 compiler, and the SDL3 build dependencies Strata needs on Linux (X11 development headers, EGL). Strata is -fetched at the commit in `strata/strata.ref`, which is the `v0.6.0` release. +fetched at the commit in `strata/strata.ref`, which is the `v0.7.0` release. ``` cmake -S strata -B build-strata -DCMAKE_BUILD_TYPE=Release diff --git a/strata/CMakeLists.txt b/strata/CMakeLists.txt index da99d93..6dfa760 100644 --- a/strata/CMakeLists.txt +++ b/strata/CMakeLists.txt @@ -7,7 +7,7 @@ # cmake --build build-strata -j4 # ctest --test-dir build-strata --output-on-failure # -# Strata is fetched at the commit in strata.ref (the v0.6.0 release), or taken +# Strata is fetched at the commit in strata.ref (the v0.7.0 release), or taken # from a local checkout with -DFETCHCONTENT_SOURCE_DIR_STRATA=. # See docs/STRATA.md. # diff --git a/strata/strata.ref b/strata/strata.ref index 5f3d703..0e9c8c8 100644 --- a/strata/strata.ref +++ b/strata/strata.ref @@ -1 +1 @@ -c2d7ee4c1102726c654c90d8361383d9ba3590b3 +20827c3d67e71c1f942c29e7cf8adfa4801188a8 diff --git a/strata/web/strata-kit/PIN b/strata/web/strata-kit/PIN index 28beecb..0e9c8c8 100644 --- a/strata/web/strata-kit/PIN +++ b/strata/web/strata-kit/PIN @@ -1 +1 @@ -901e6c97bc70c86c0e7dfbf857bb87ec405e0164 +20827c3d67e71c1f942c29e7cf8adfa4801188a8 diff --git a/strata/web/strata-kit/SHA256SUMS b/strata/web/strata-kit/SHA256SUMS index a5be9e6..2e484d6 100644 --- a/strata/web/strata-kit/SHA256SUMS +++ b/strata/web/strata-kit/SHA256SUMS @@ -1,2 +1,2 @@ -7da2a43f932f63e5ae86273a7675b55180dbde4d61dc6e0c5fcd048026879946 strata-loader.js +fa03e8ed92759359359f29f40668c947e63b0c9f3eabafcce2f911110637f5c3 strata-loader.js 8e3fc647b4775773eabc147e564ffbddd0653b252eff96c5432213ee362798df LICENSE diff --git a/strata/web/strata-kit/strata-loader.js b/strata/web/strata-kit/strata-loader.js index f7d1d46..3ac65ee 100644 --- a/strata/web/strata-kit/strata-loader.js +++ b/strata/web/strata-kit/strata-loader.js @@ -17,6 +17,21 @@ // ... create the scene with StrataKit.create({module: mod, ...}), then: StrataLoader.done(); + A host whose own loader already knows how far it has got (a .NET or + Unity-style boot that fetches its own files) shows the same overlay and + drives its bar itself: + + if (StrataLoader.show({title: "the game", poster: "/poster.jpg"})) { + StrataLoader.progress(0.4, "Loading assemblies"); // 0..1, label optional + // ... boot; then: + StrataLoader.done(); + } + + show() takes load()'s display options (title, poster, trailer, fallback, + mount) and returns false, with the fallback showing, when WebGL 2 is + missing. progress(fraction, label) clamps the fraction to 0..1 and never + moves the bar back; the label (or the percentage) replaces the size line. + What it shows, in an overlay it adds to the page (class sk-loader): - the poster (if any) as a blurred backdrop, and the title; @@ -73,7 +88,7 @@ @media (prefers-reduced-motion: reduce){.sk-loader,.sk-loader .sk-fill{transition:none} .sk-loader .sk-bar.sk-indeterminate .sk-fill{animation:none;width:100%;opacity:.5}}`; - let root = null, opts = {}, state = "idle"; + let root = null, opts = {}, state = "idle", shown = 0; function el(tag, cls, text) { const e = document.createElement(tag); @@ -115,12 +130,14 @@ return root; } - function progress(got, total) { + // Sets the bar to `fraction` (0..1), or to indeterminate when it is null, + // and the line under it to `text`. Both progress paths draw through here. + function setBar(fraction, text) { if (!root) return; const bar = root.querySelector(".sk-bar"), fill = root.querySelector(".sk-fill"); if (!bar) return; - if (total) { - const pct = Math.min(100, Math.round(got / total * 100)); + if (fraction != null) { + const pct = Math.min(100, Math.round(fraction * 100)); bar.classList.remove("sk-indeterminate"); fill.style.width = pct + "%"; bar.setAttribute("aria-valuenow", String(pct)); @@ -128,7 +145,42 @@ bar.classList.add("sk-indeterminate"); bar.removeAttribute("aria-valuenow"); } - root.querySelector(".sk-size").textContent = sizeText(got, total); + root.querySelector(".sk-size").textContent = text; + } + + // load()'s byte progress: `got` of `total` bytes (total 0 when unknown). + function byteProgress(got, total) { + setBar(total ? got / total : null, sizeText(got, total)); + } + + // A host's own progress: `fraction` of the way (clamped to 0..1, and never + // lower than the last call's), with an optional label for the line under + // the bar (else the percentage). Mounts the overlay if show() or load() has + // not; does nothing once the shell is done or showing the fallback. + function progress(fraction, label) { + if (state === "done" || state === "fallback") return; + const f = Number(fraction); + if (Number.isNaN(f)) return; + shown = Math.max(shown, Math.min(1, Math.max(0, f))); + if (state === "idle") state = "loading"; + mount(); + setBar(shown, label != null && label !== "" ? String(label) : `${Math.round(shown * 100)}%`); + const bar = root.querySelector(".sk-bar"); + if (bar) { + if (label) bar.setAttribute("aria-valuetext", String(label)); + else bar.removeAttribute("aria-valuetext"); + } + } + + // The overlay without load()'s fetch, for a host that boots its own way. + // Returns false (the fallback showing) when WebGL 2 is missing. + function show(o) { + opts = Object.assign({}, o); + state = "loading"; + shown = 0; + mount(); + if (!hasWebGL2()) { fallback("no-webgl2"); return false; } + return true; } function hasWebGL2() { @@ -219,25 +271,25 @@ let total = (!enc || enc === "identity") && header > 0 ? header : (statedBytes || 0); if (!res.body || !res.body.getReader) { const buf = await res.arrayBuffer(); - progress(buf.byteLength, buf.byteLength); + byteProgress(buf.byteLength, buf.byteLength); return buf; } const reader = res.body.getReader(); const chunks = []; let got = 0; - progress(0, total); + byteProgress(0, total); for (;;) { const {done, value} = await reader.read(); if (done) break; chunks.push(value); got += value.byteLength; if (total && got > total) total = 0; // the stated size was wrong: say bytes only - progress(got, total); + byteProgress(got, total); } const out = new Uint8Array(got); let o = 0; for (const c of chunks) { out.set(c, o); o += c.byteLength; } - progress(got, got); + byteProgress(got, got); return out.buffer; } @@ -247,8 +299,9 @@ const keep = [...box.childNodes]; box.textContent = ""; const msg = reason === "save-data" ? "Data saver is on." - : reason === "low-memory" ? "This device has little memory." : "Motion is reduced on this device."; - box.appendChild(el("p", "sk-msg", `${msg} The live view downloads ${opts.bytes ? mb(opts.bytes) + " MB" : "its engine"} and animates.`)); + : reason === "low-memory" ? "This device has little memory." : "This device asks for reduced motion."; + const size = opts.bytes ? `a ${mb(opts.bytes)} MB download` : "a download"; + box.appendChild(el("p", "sk-msg", `${msg} The live view is ${size} and is animated.`)); const go = el("button", "", "Start the live view"); go.type = "button"; go.addEventListener("click", () => { box.textContent = ""; keep.forEach((n) => box.appendChild(n)); resolve(); }); @@ -267,6 +320,7 @@ async function load(o) { opts = Object.assign({}, o); state = "loading"; + shown = 0; mount(); if (!hasWebGL2()) { fallback("no-webgl2"); @@ -311,9 +365,11 @@ } // The kit could not go any lower and frames are still slow: offer the clip - // (or the fallback link) without taking the live view away. + // (or the fallback link) without taking the live view away. Once: a second + // call while the bar is up does nothing. function slow() { if (state === "fallback" || (!opts.trailer && !(opts.fallback && opts.fallback.href))) return; + if (document.querySelector(".sk-slow")) return; const bar = el("div", "sk-slow"); bar.setAttribute("role", "status"); bar.style.cssText = "position:fixed;left:50%;transform:translateX(-50%);bottom:max(12px,env(safe-area-inset-bottom));" + @@ -337,5 +393,5 @@ document.body.appendChild(bar); } - window.StrataLoader = {load, done, fallback, slow, hasWebGL2, get state() { return state; }}; + window.StrataLoader = {load, show, progress, done, fallback, slow, hasWebGL2, get state() { return state; }}; })();