use wasm_bindgen::prelude::*;
use std::sync::Arc;
use web_sys::HtmlCanvasElement;
use crate::asset::MemoryAssetResolver;
use crate::dsl::{GraphScript, is_graph_script, parse_graph_script};
use crate::process::cpu_renderer::render_process_frame_cpu;
#[cfg(target_arch = "wasm32")]
use crate::process::wasm_webgpu::render_process_frame_to_canvas_gpu as render_process_frame_to_canvas_gpu_impl;
use crate::scene::render::{SceneRenderProfile, SceneRenderer, render_scene_graph_frame};
use crate::world::{WorldFrameRenderer, is_world_graph_script, parse_world_graph_script};
fn js_error(message: String) -> JsValue {
js_sys::Error::new(&message).into()
}
#[wasm_bindgen]
pub fn motionloom_parse_summary(script: &str) -> Result<String, JsValue> {
if is_graph_script(script) {
let graph = parse_graph_script(script).map_err(|err| js_error(err.to_string()))?;
return Ok(format!(
"scene graph: {} scene node(s), {} frame(s)",
graph.scene_nodes.len(),
graph.duration_ms
));
}
if is_world_graph_script(script) {
let graph = parse_world_graph_script(script).map_err(|err| js_error(err.to_string()))?;
return Ok(format!(
"world graph: {} world node(s), {} frame(s)",
graph.worlds.len(),
graph.duration_ms
));
}
Err(js_error(
"script does not look like a scene or world graph".to_string(),
))
}
#[wasm_bindgen]
pub async fn motionloom_render_scene_frame(
script: &str,
frame: u32,
width: u32,
height: u32,
) -> Result<Vec<u8>, JsValue> {
motionloom_render_scene_frame_with_profile(script, frame, width, height, "gpu-cpu").await
}
#[wasm_bindgen]
pub async fn motionloom_render_scene_frame_with_profile(
script: &str,
frame: u32,
width: u32,
height: u32,
profile: &str,
) -> Result<Vec<u8>, JsValue> {
let mut graph = parse_graph_script(script).map_err(|err| js_error(err.to_string()))?;
graph.size.0 = width.max(1);
graph.size.1 = height.max(1);
let (preferred, fallback) = parse_scene_profile_with_fallback(profile);
let mut last_err = None;
for profile in [Some(preferred), fallback].into_iter().flatten() {
match render_scene_graph_frame(&graph, frame, profile).await {
Ok(image) => return Ok(image.into_raw()),
Err(err) => last_err = Some(err),
}
}
Err(js_error(
last_err
.map(|err| err.to_string())
.unwrap_or_else(|| "scene render failed".to_string()),
))
}
#[wasm_bindgen]
pub async fn motionloom_render_scene_frame_to_canvas_gpu(
script: &str,
frame: u32,
width: u32,
height: u32,
canvas: HtmlCanvasElement,
) -> Result<(), JsValue> {
let mut graph = parse_graph_script(script).map_err(|err| js_error(err.to_string()))?;
graph.size.0 = width.max(1);
graph.size.1 = height.max(1);
graph.render_size = Some((width.max(1), height.max(1)));
let mut renderer = SceneRenderer::new(SceneRenderProfile::Gpu)
.await
.map_err(|err| js_error(err.to_string()))?;
renderer
.render_frame_to_canvas(&graph, frame, canvas)
.await
.map_err(|err| js_error(err.to_string()))
}
#[wasm_bindgen]
pub async fn motionloom_webgpu_debug_solid_to_canvas(
canvas: HtmlCanvasElement,
width: u32,
height: u32,
) -> Result<(), JsValue> {
let mut renderer = SceneRenderer::new(SceneRenderProfile::Gpu)
.await
.map_err(|err| js_error(err.to_string()))?;
renderer
.debug_solid_to_canvas(canvas, width, height, [0.1, 0.85, 0.25, 1.0])
.await
.map_err(|err| js_error(err.to_string()))
}
#[wasm_bindgen]
pub async fn motionloom_webgpu_debug_uploaded_texture_to_canvas(
canvas: HtmlCanvasElement,
width: u32,
height: u32,
) -> Result<(), JsValue> {
let mut renderer = SceneRenderer::new(SceneRenderProfile::Gpu)
.await
.map_err(|err| js_error(err.to_string()))?;
renderer
.debug_uploaded_texture_to_canvas(canvas, width, height, [32, 96, 255, 255])
.await
.map_err(|err| js_error(err.to_string()))
}
#[wasm_bindgen]
pub async fn motionloom_webgpu_debug_empty_scene_texture_to_canvas(
canvas: HtmlCanvasElement,
width: u32,
height: u32,
) -> Result<(), JsValue> {
let mut renderer = SceneRenderer::new(SceneRenderProfile::Gpu)
.await
.map_err(|err| js_error(err.to_string()))?;
renderer
.debug_empty_scene_texture_to_canvas(canvas, width, height)
.await
.map_err(|err| js_error(err.to_string()))
}
#[wasm_bindgen]
pub fn motionloom_render_process_frame(
script: &str,
frame: u32,
width: u32,
height: u32,
rgba: &[u8],
) -> Result<Vec<u8>, JsValue> {
render_process_frame_cpu(script, frame, width, height, rgba)
.map(|image| image.into_raw())
.map_err(|err| js_error(err.to_string()))
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub async fn motionloom_render_process_frame_to_canvas_gpu(
script: &str,
frame: u32,
width: u32,
height: u32,
rgba: &[u8],
canvas: HtmlCanvasElement,
) -> Result<(), JsValue> {
render_process_frame_to_canvas_gpu_impl(script, frame, width, height, rgba, canvas)
.await
.map_err(|err| js_error(err.to_string()))
}
fn parse_scene_profile_with_fallback(
profile: &str,
) -> (SceneRenderProfile, Option<SceneRenderProfile>) {
match profile.to_ascii_lowercase().as_str() {
"cpu" => (SceneRenderProfile::Cpu, None),
"gpu" => (SceneRenderProfile::Gpu, None),
"gpu-cpu" => (SceneRenderProfile::Gpu, Some(SceneRenderProfile::Cpu)),
_ => (SceneRenderProfile::Gpu, Some(SceneRenderProfile::Cpu)),
}
}
#[wasm_bindgen]
pub fn motionloom_render_world_frame(
script: &str,
frame: u32,
asset_root: &str,
) -> Result<Vec<u8>, JsValue> {
let graph = parse_world_graph_script(script).map_err(|err| js_error(err.to_string()))?;
let mut renderer = WorldFrameRenderer::new();
let image = renderer
.render_frame(&graph, frame, asset_root)
.map_err(|err| js_error(err.to_string()))?;
Ok(image.into_raw())
}
#[wasm_bindgen]
pub fn motionloom_document_type(script: &str) -> String {
if is_graph_script(script) {
"scene".to_string()
} else if is_world_graph_script(script) {
"world".to_string()
} else {
"unknown".to_string()
}
}
#[wasm_bindgen]
pub struct WasmSceneRenderer {
graph: GraphScript,
profile: SceneRenderProfile,
resolver: Arc<MemoryAssetResolver>,
renderer: Option<SceneRenderer>,
}
#[wasm_bindgen]
impl WasmSceneRenderer {
#[wasm_bindgen(constructor)]
pub fn new(script: &str, profile: &str) -> Result<WasmSceneRenderer, JsValue> {
let graph = parse_graph_script(script).map_err(|err| js_error(err.to_string()))?;
let profile = parse_profile(profile)?;
Ok(Self {
graph,
profile,
resolver: Arc::new(MemoryAssetResolver::new()),
renderer: None,
})
}
pub async fn create(script: &str, profile: &str) -> Result<WasmSceneRenderer, JsValue> {
let graph = parse_graph_script(script).map_err(|err| js_error(err.to_string()))?;
let profile = parse_profile(profile)?;
let resolver = Arc::new(MemoryAssetResolver::new());
let renderer = SceneRenderer::with_resolver(profile, resolver.clone())
.await
.map_err(|err| js_error(err.to_string()))?;
Ok(Self {
graph,
profile,
resolver,
renderer: Some(renderer),
})
}
pub fn add_asset(&mut self, name: &str, bytes: &[u8]) {
self.resolver.insert(name.to_string(), bytes.to_vec());
}
pub async fn add_font(&mut self, bytes: &[u8]) -> Result<(), JsValue> {
if self.renderer.is_none() {
self.renderer = Some(
SceneRenderer::with_resolver(self.profile, self.resolver.clone())
.await
.map_err(|err| js_error(err.to_string()))?,
);
}
let renderer = self
.renderer
.as_mut()
.ok_or_else(|| js_error("scene renderer was not initialized".to_string()))?;
renderer.load_font_data(bytes.to_vec());
Ok(())
}
pub fn clear_assets(&mut self) {
self.resolver.clear();
}
pub async fn render_frame(&mut self, frame: u32) -> Result<Vec<u8>, JsValue> {
if self.renderer.is_none() {
self.renderer = Some(
SceneRenderer::with_resolver(self.profile, self.resolver.clone())
.await
.map_err(|err| js_error(err.to_string()))?,
);
}
let renderer = self
.renderer
.as_mut()
.ok_or_else(|| js_error("scene renderer was not initialized".to_string()))?;
let image = renderer
.render_frame(&self.graph, frame)
.await
.map_err(|err| js_error(err.to_string()))?;
Ok(image.into_raw())
}
pub async fn render_frame_to_canvas(
&mut self,
frame: u32,
canvas: HtmlCanvasElement,
) -> Result<(), JsValue> {
if self.profile != SceneRenderProfile::Gpu {
return Err(js_error(
"render_frame_to_canvas requires a gpu WasmSceneRenderer".to_string(),
));
}
if self.renderer.is_none() {
self.renderer = Some(
SceneRenderer::with_resolver(self.profile, self.resolver.clone())
.await
.map_err(|err| js_error(err.to_string()))?,
);
}
let renderer = self
.renderer
.as_mut()
.ok_or_else(|| js_error("scene renderer was not initialized".to_string()))?;
renderer
.render_frame_to_canvas(&self.graph, frame, canvas)
.await
.map_err(|err| js_error(err.to_string()))
}
pub async fn debug_solid_to_canvas(
&mut self,
canvas: HtmlCanvasElement,
width: u32,
height: u32,
) -> Result<(), JsValue> {
if self.profile != SceneRenderProfile::Gpu {
return Err(js_error(
"debug_solid_to_canvas requires a gpu WasmSceneRenderer".to_string(),
));
}
if self.renderer.is_none() {
self.renderer = Some(
SceneRenderer::with_resolver(self.profile, self.resolver.clone())
.await
.map_err(|err| js_error(err.to_string()))?,
);
}
let renderer = self
.renderer
.as_mut()
.ok_or_else(|| js_error("scene renderer was not initialized".to_string()))?;
renderer
.debug_solid_to_canvas(canvas, width, height, [0.1, 0.85, 0.25, 1.0])
.await
.map_err(|err| js_error(err.to_string()))
}
pub async fn debug_uploaded_texture_to_canvas(
&mut self,
canvas: HtmlCanvasElement,
width: u32,
height: u32,
) -> Result<(), JsValue> {
if self.profile != SceneRenderProfile::Gpu {
return Err(js_error(
"debug_uploaded_texture_to_canvas requires a gpu WasmSceneRenderer".to_string(),
));
}
if self.renderer.is_none() {
self.renderer = Some(
SceneRenderer::with_resolver(self.profile, self.resolver.clone())
.await
.map_err(|err| js_error(err.to_string()))?,
);
}
let renderer = self
.renderer
.as_mut()
.ok_or_else(|| js_error("scene renderer was not initialized".to_string()))?;
renderer
.debug_uploaded_texture_to_canvas(canvas, width, height, [32, 96, 255, 255])
.await
.map_err(|err| js_error(err.to_string()))
}
pub async fn debug_empty_scene_texture_to_canvas(
&mut self,
canvas: HtmlCanvasElement,
width: u32,
height: u32,
) -> Result<(), JsValue> {
if self.profile != SceneRenderProfile::Gpu {
return Err(js_error(
"debug_empty_scene_texture_to_canvas requires a gpu WasmSceneRenderer".to_string(),
));
}
if self.renderer.is_none() {
self.renderer = Some(
SceneRenderer::with_resolver(self.profile, self.resolver.clone())
.await
.map_err(|err| js_error(err.to_string()))?,
);
}
let renderer = self
.renderer
.as_mut()
.ok_or_else(|| js_error("scene renderer was not initialized".to_string()))?;
renderer
.debug_empty_scene_texture_to_canvas(canvas, width, height)
.await
.map_err(|err| js_error(err.to_string()))
}
#[wasm_bindgen(getter)]
pub fn total_frames(&self) -> u32 {
let fps = self.graph.fps.max(1.0);
let duration_sec = (self.graph.duration_ms as f32 / 1000.0).max(1.0 / fps);
(duration_sec * fps).round() as u32
}
}
#[wasm_bindgen]
pub struct WasmWorldRenderer {
graph: crate::world::WorldGraph,
resolver: Arc<MemoryAssetResolver>,
}
#[wasm_bindgen]
impl WasmWorldRenderer {
#[wasm_bindgen(constructor)]
pub fn new(script: &str) -> Result<WasmWorldRenderer, JsValue> {
let graph = parse_world_graph_script(script).map_err(|err| js_error(err.to_string()))?;
Ok(Self {
graph,
resolver: Arc::new(MemoryAssetResolver::new()),
})
}
pub fn add_asset(&mut self, name: &str, bytes: &[u8]) {
self.resolver.insert(name.to_string(), bytes.to_vec());
}
pub fn clear_assets(&mut self) {
self.resolver.clear();
}
pub fn render_frame(&mut self, frame: u32, asset_root: &str) -> Result<Vec<u8>, JsValue> {
let mut renderer = WorldFrameRenderer::with_resolver(self.resolver.clone());
let image = renderer
.render_frame(&self.graph, frame, asset_root)
.map_err(|err| js_error(err.to_string()))?;
Ok(image.into_raw())
}
}
fn parse_profile(profile: &str) -> Result<SceneRenderProfile, JsValue> {
match profile.to_ascii_lowercase().as_str() {
"cpu" => Ok(SceneRenderProfile::Cpu),
"gpu" => Ok(SceneRenderProfile::Gpu),
_ => Err(js_error(format!("unknown render profile: {profile}"))),
}
}