use std::cell::RefCell;
use std::collections::{BTreeMap, VecDeque};
use oxframe_core::FrameSource;
use oxframe_preview::{Output, PreviewEngine, Session, rgba_encoder};
#[cfg(feature = "gpu")]
use oxframe_protocol::FrameFormat;
use oxframe_protocol::{Command, Meta};
use oxframe_render::{CpuRenderer, Renderer};
#[cfg(feature = "gpu")]
use oxframe_render::{Image, RenderOptions};
use serde::Deserialize;
use wasm_bindgen::prelude::*;
use crate::{Engine, PreviewAdapter, Project};
thread_local! {
static PROJECT: RefCell<Option<fn() -> Project>> = const { RefCell::new(None) };
}
pub fn register(project: fn() -> Project) {
console_error_panic_hook::set_once();
PROJECT.with(|p| *p.borrow_mut() = Some(project));
}
#[derive(Deserialize, Default)]
struct Config {
#[serde(default)]
metas: BTreeMap<String, Meta>,
}
#[wasm_bindgen]
pub struct WasmEngine {
session: Session<PreviewAdapter>,
events: VecDeque<String>,
frames: VecDeque<Vec<u8>>,
}
fn project_engine(config_json: &str) -> Result<Engine, JsError> {
let project = PROJECT
.with(|p| *p.borrow())
.ok_or_else(|| JsError::new("no project registered: call oxframe::wasm::register()"))?;
let config: Config = if config_json.trim().is_empty() {
Config::default()
} else {
serde_json::from_str(config_json).map_err(|e| JsError::new(&format!("bad config: {e}")))?
};
Engine::new(project().inline_meta(config.metas)).map_err(|e| JsError::new(&e.to_string()))
}
#[wasm_bindgen]
pub fn init(config_json: &str) -> Result<WasmEngine, JsError> {
let engine = project_engine(config_json)?;
let renderer: Box<dyn Renderer> = Box::new(CpuRenderer::new());
Ok(WasmEngine {
session: Session::new(PreviewAdapter::new(engine, renderer), rgba_encoder(), 128),
events: VecDeque::new(),
frames: VecDeque::new(),
})
}
#[cfg(feature = "gpu")]
struct CanvasRenderer(oxframe_render::GpuRenderer);
#[cfg(feature = "gpu")]
impl Renderer for CanvasRenderer {
fn render(&mut self, graph: &oxframe_2d::SceneGraph, options: &RenderOptions) -> Image {
self.0.render_to_canvas(graph, options);
Image {
width: options.width,
height: options.height,
data: Vec::new(),
}
}
}
#[cfg(feature = "gpu")]
#[wasm_bindgen]
pub async fn init_gpu(
config_json: String,
canvas: web_sys::OffscreenCanvas,
) -> Result<WasmEngine, JsError> {
let engine = project_engine(&config_json)?;
let gpu = oxframe_render::GpuRenderer::new_for_canvas(canvas)
.await
.map_err(|e| JsError::new(&e.to_string()))?;
let renderer: Box<dyn Renderer> = Box::new(CanvasRenderer(gpu));
let encoder: oxframe_preview::Encoder =
Box::new(|_img: &Image| (FrameFormat::Surface, Vec::new()));
Ok(WasmEngine {
session: Session::new(PreviewAdapter::new(engine, renderer), encoder, 0),
events: VecDeque::new(),
frames: VecDeque::new(),
})
}
#[wasm_bindgen]
impl WasmEngine {
fn push(&mut self, outputs: Vec<Output>) {
for o in outputs {
match o {
Output::Event(e) => self
.events
.push_back(serde_json::to_string(&e).expect("event json")),
Output::Frame(f) => {
self.frames.clear();
self.frames.push_back(f);
}
}
}
}
pub fn command(&mut self, json: &str) -> Result<(), JsError> {
let cmd: Command =
serde_json::from_str(json).map_err(|e| JsError::new(&format!("bad command: {e}")))?;
let now = js_now();
let out = self.session.handle(cmd, now);
self.push(out);
Ok(())
}
pub fn tick(&mut self, now: f64) {
let out = self.session.tick(now);
self.push(out);
}
pub fn poll_events(&mut self) -> Vec<String> {
self.events.drain(..).collect()
}
pub fn poll_frame(&mut self) -> Option<Vec<u8>> {
self.frames.pop_front()
}
pub fn render_frame(&mut self, frame: u32) -> Vec<u8> {
let size = self.session.engine().size();
self.session.engine().render(frame, size).data
}
pub fn info(&mut self) -> String {
let e = self.session.engine();
let (w, h) = e.size();
serde_json::json!({ "fps": e.fps(), "size": [w, h], "frames": e.total_frames() })
.to_string()
}
pub fn meta(&mut self) -> String {
let metas: BTreeMap<String, Meta> = self.session.engine().metas().into_iter().collect();
serde_json::to_string(&metas).expect("meta json")
}
}
fn js_now() -> f64 {
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = performance, js_name = now)]
fn performance_now() -> f64;
}
performance_now() / 1000.0
}