mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 19:47:12 -06:00
M15.2: web app depth (WEB-1): browser storage, Web Audio, worker renders, WebGPU viewer, PWA
- Persistence: settings, shortcuts, recent projects, imported media, saved projects and auto-saves live in OPFS (IndexedDB fallback) through an in-memory mirror flushed in the background (apps/effectcraft-web/src/store.rs, persist.rs); ConfigStore gains FileOps so auto-save / recovery work in the browser store; the session snapshot restores the last project + editor state on reload; File > Open Recent works from storage. - Audio: Web Audio output (AudioWorklet, ScriptProcessor fallback) fed by AudioPlayback::pump on wasm32; A/V sync from AudioContext.currentTime; meters; unlock on first gesture. - Background jobs: engine::offload (WorkerRequest/WorkerReply protocol, Offload trait, run_request); Render Queue, tracker, mask tracker, Warp Stabilizer and Roto Freeze run in Web Workers (a second engine instance on the shared compiled module), progress streamed back; cancel terminates the worker. - GPU: WebGPU viewer frames drawn straight from the compositor's texture (gpu_display on wasm32 when the viewer draws plainly); async readback (Gpu::read_display_async) for pixels. - PWA: manifest, service worker precaching the build (xtask fills list/version), icons copied from assets/app-icon; COOP/COEP kept from cached responses; offline reload works. - window.effectcraft: saveToBrowser, listStored, removeStored, flush; info() reports storage, audio, gpu, workers. - Tests: native store + protocol + offload (render, track, cancel) tests; smoke.mjs extended (GPU path, worker render responsiveness, audio gesture + playback, reload persistence, Open Recent, offline reload). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c406b71cba
commit
0bee1ab2da
+44
-2
@@ -285,8 +285,14 @@ fn web(args: &[String]) -> Result<(), String> {
|
||||
std::fs::copy(e.path(), dist.join(&name)).map_err(|e| format!("{name}: {e}"))?;
|
||||
}
|
||||
}
|
||||
// PWA icons: the app icon (assets/app-icon, attributed there).
|
||||
for n in [256, 512] {
|
||||
let src = format!("assets/app-icon/hicolor/{n}x{n}/apps/ai.storyteller.effectcraft.png");
|
||||
std::fs::copy(&src, dist.join(format!("icon-{n}.png"))).map_err(|e| format!("{src}: {e}"))?;
|
||||
}
|
||||
service_worker(&dist)?;
|
||||
let size = |p: &std::path::Path| std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
|
||||
let total: u64 = std::fs::read_dir(&dist).map_err(|e| e.to_string())?.flatten().map(|e| size(&e.path())).sum();
|
||||
let total: u64 = dist_files(&dist).iter().map(|f| size(&dist.join(f))).sum();
|
||||
println!("web: {} ({:.1} MB wasm, {:.1} MB total)", dist.display(), size(&bg) as f64 / 1e6, total as f64 / 1e6);
|
||||
if let Some(port) = serve {
|
||||
serve_dir(&dist, port)?;
|
||||
@@ -294,6 +300,42 @@ fn web(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Every file under `dist`, as `/`-separated relative paths, sorted.
|
||||
fn dist_files(dist: &std::path::Path) -> Vec<String> {
|
||||
fn walk(dir: &std::path::Path, rel: &str, out: &mut Vec<String>) {
|
||||
for e in std::fs::read_dir(dir).into_iter().flatten().flatten() {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
let r = if rel.is_empty() { name } else { format!("{rel}/{name}") };
|
||||
if e.path().is_dir() {
|
||||
walk(&e.path(), &r, out);
|
||||
} else {
|
||||
out.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut v = vec![];
|
||||
walk(dist, "", &mut v);
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
/// Fill the service worker's precache list and version (a hash of the build's files).
|
||||
fn service_worker(dist: &std::path::Path) -> Result<(), String> {
|
||||
let sw = dist.join("sw.js");
|
||||
let text = std::fs::read_to_string(&sw).map_err(|e| format!("sw.js: {e}"))?;
|
||||
let files: Vec<String> = dist_files(dist).into_iter().filter(|f| f != "sw.js").collect();
|
||||
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
for f in &files {
|
||||
for b in f.bytes().chain(std::fs::read(dist.join(f)).map_err(|e| format!("{f}: {e}"))?) {
|
||||
h ^= b as u64;
|
||||
h = h.wrapping_mul(0x0100_0000_01b3);
|
||||
}
|
||||
}
|
||||
let list: Vec<String> = std::iter::once("./".to_string()).chain(files.iter().map(|f| format!("./{f}"))).map(|f| format!("{f:?}")).collect();
|
||||
let text = text.replace("__EC_VERSION__", &format!("{h:016x}")).replace("__EC_FILES__", &format!("[{}]", list.join(", ")));
|
||||
std::fs::write(&sw, text).map_err(|e| format!("sw.js: {e}"))
|
||||
}
|
||||
|
||||
/// A tiny static file server for the web build (localhost only). Sends the cross-origin
|
||||
/// isolation headers so `crossOriginIsolated` is true, like a production deployment would.
|
||||
fn serve_dir(dir: &std::path::Path, port: u16) -> Result<(), String> {
|
||||
@@ -330,7 +372,7 @@ fn serve_dir(dir: &std::path::Path, port: u16) -> Result<(), String> {
|
||||
"js" => "text/javascript",
|
||||
"wasm" => "application/wasm",
|
||||
"svg" => "image/svg+xml",
|
||||
"json" => "application/json",
|
||||
"json" | "webmanifest" => "application/json",
|
||||
"png" => "image/png",
|
||||
"gif" => "image/gif",
|
||||
"ecproj" => "application/json",
|
||||
|
||||
Reference in New Issue
Block a user