use super::*;
pub(crate) fn use_lighting_state() -> UseLighting {
UseLighting {
fps: App::use_signal(|| 0.0),
running: App::use_signal(|| true),
loaded: App::use_signal(|| false),
active: App::use_signal(|| false),
loop_started: App::use_signal(|| false),
render_scale: App::use_signal(|| 1.0),
init_error_code: App::use_signal(|| ""),
}
}
pub(crate) fn use_lighting_webgl_state() -> UseLightingWebGl {
UseLightingWebGl {
fps: App::use_signal(|| 0.0),
running: App::use_signal(|| true),
loaded: App::use_signal(|| false),
active: App::use_signal(|| false),
loop_started: App::use_signal(|| false),
init_error_code: App::use_signal(|| ""),
}
}
pub(crate) fn use_lighting_webgpu_state() -> UseLightingWebGpu {
UseLightingWebGpu {
fps: App::use_signal(|| 0.0),
running: App::use_signal(|| true),
loaded: App::use_signal(|| false),
active: App::use_signal(|| false),
loop_started: App::use_signal(|| false),
init_error_code: App::use_signal(|| ""),
}
}
pub(crate) fn use_lighting_fullscreen_state() -> UseLightingFullscreen {
UseLightingFullscreen {
canvas_2d: App::use_signal(|| false),
web_gl: App::use_signal(|| false),
web_gpu: App::use_signal(|| false),
}
}
fn lighting_canvas_detached(canvas_selector: &str) -> bool {
window()
.and_then(|window_value: Window| window_value.document())
.and_then(|document: Document| document.query_selector(canvas_selector).ok().flatten())
.is_none()
}
fn read_lighting_canvas_size(canvas_selector: &str) -> Option<(f64, f64)> {
let window_value: Window = window()?;
let document_value: Document = window_value.document()?;
let element: Element = document_value
.query_selector(canvas_selector)
.ok()
.flatten()?;
let canvas: HtmlCanvasElement = element.unchecked_into();
let rect: DomRect = canvas.get_bounding_client_rect();
Some((rect.width(), rect.height()))
}
fn acquire_lighting_canvas(
width: u32,
height: u32,
) -> Option<(HtmlCanvasElement, CanvasRenderingContext2d)> {
let window_value: Window = window()?;
let document_value: Document = window_value.document()?;
let element: Element = document_value
.query_selector(LIGHTING_CANVAS_SELECTOR)
.ok()
.flatten()?;
let canvas: HtmlCanvasElement = element.unchecked_into();
if canvas.width() != width {
canvas.set_width(width);
}
if canvas.height() != height {
canvas.set_height(height);
}
let context_object: Object = canvas.get_context(LIGHTING_CONTEXT_TYPE).ok().flatten()?;
let context: CanvasRenderingContext2d = context_object.unchecked_into();
Some((canvas, context))
}
fn build_lighting_scene() -> (Vec<LightingSphere>, LightingUniforms) {
let width: f64 = LIGHTING_WIDTH;
let height: f64 = LIGHTING_HEIGHT;
let red_material: Material = Material::phong(Vector3D::new(0.85, 0.20, 0.20), 0.5, 24.0);
let green_material: Material = Material::phong(Vector3D::new(0.20, 0.80, 0.30), 0.6, 32.0);
let blue_material: Material = Material::phong(Vector3D::new(0.25, 0.45, 0.95), 0.4, 18.0);
let yellow_material: Material = Material::phong(Vector3D::new(0.95, 0.85, 0.20), 0.7, 48.0);
let magenta_material: Material = Material::lambert(Vector3D::new(0.85, 0.25, 0.75));
let spheres: Vec<LightingSphere> = vec![
LightingSphere {
cx: width * 0.22,
cy: height * 0.42,
radius: 24.0,
material: red_material.clone(),
},
LightingSphere {
cx: width * 0.42,
cy: height * 0.55,
radius: 18.0,
material: green_material.clone(),
},
LightingSphere {
cx: width * 0.62,
cy: height * 0.40,
radius: 22.0,
material: blue_material.clone(),
},
LightingSphere {
cx: width * 0.78,
cy: height * 0.62,
radius: 16.0,
material: yellow_material.clone(),
},
LightingSphere {
cx: width * 0.50,
cy: height * 0.20,
radius: 12.0,
material: magenta_material,
},
];
let eye: Vector3D = Vector3D::new(0.0, 0.0, LIGHTING_EYE_Z);
let mut lights: LightingUniforms = LightingUniforms::with_eye(eye);
lights.set_ambient(Vector3D::new(0.08, 0.08, 0.10));
let sun: Light = Light::new_directional(
Vector3D::new(-0.45, -0.55, -0.70),
Vector3D::new(1.00, 0.95, 0.85),
);
let lamp: Light = Light::new_point(
Vector3D::new(width * 0.5, -10.0, 1.2),
Vector3D::new(0.40, 0.70, 1.00),
1.4,
);
lights.add_light(sun);
lights.add_light(lamp);
(spheres, lights)
}
fn gamma_byte(value: f64) -> u8 {
let clamped: f64 = value.clamp(0.0, 1.0);
(clamped.powf(1.0 / 2.2) * 255.0).round() as u8
}
fn lighting_scaled_dimensions(scale: f64) -> (u32, u32) {
let width: u32 = (LIGHTING_WIDTH * scale).round() as u32;
let height: u32 = (LIGHTING_HEIGHT * scale).round() as u32;
(width, height)
}
fn render_lighting_frame(
buffer: &mut [u8],
width: u32,
height: u32,
scale: f64,
spheres: &[LightingSphere],
lights: &LightingUniforms,
) {
let occluders: [(Vector3D, f64); 0] = [];
let ground_material: Material = Material::phong(Vector3D::new(0.55, 0.55, 0.60), 0.15, 12.0);
let ground_y: f64 = (LIGHTING_HEIGHT * 0.78) as i32 as f64;
let inv_scale: f64 = 1.0 / scale;
let sub_offsets: [(f64, f64); 4] = [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)];
let mut index: usize = 0;
for y in 0..height {
for x in 0..width {
let mut red: f64 = 0.0;
let mut green: f64 = 0.0;
let mut blue: f64 = 0.0;
let mut alpha: u8 = 0;
let logical_x: f64 = (f64::from(x) + 0.5) * inv_scale;
let logical_y: f64 = (f64::from(y) + 0.5) * inv_scale;
if logical_y >= ground_y && logical_y < ground_y + 1.0 {
let position: Vector3D = Vector3D::new(logical_x.floor(), ground_y, 0.0);
let normal: Vector3D = Vector3D::new(0.0, -1.0, 0.0);
let color: Vector3D = lights.shade(position, normal, &ground_material, &occluders);
red = color.get_x();
green = color.get_y();
blue = color.get_z();
alpha = 255;
}
for sphere in spheres.iter() {
let radius_squared: f64 = sphere.radius * sphere.radius;
let mut inside_count: u32 = 0;
let mut sum_r: f64 = 0.0;
let mut sum_g: f64 = 0.0;
let mut sum_b: f64 = 0.0;
for (dx_off, dy_off) in sub_offsets {
let sample_x: f64 = (f64::from(x) + dx_off) * inv_scale;
let sample_y: f64 = (f64::from(y) + dy_off) * inv_scale;
let dx: f64 = sample_x - sphere.cx;
let dy: f64 = sample_y - sphere.cy;
let distance_squared: f64 = dx * dx + dy * dy;
if distance_squared > radius_squared {
continue;
}
inside_count += 1;
let dz: f64 = (radius_squared - distance_squared).max(0.0).sqrt();
let normal: Vector3D =
Vector3D::new(dx / sphere.radius, dy / sphere.radius, dz / sphere.radius);
let position: Vector3D = Vector3D::new(sample_x, sample_y, dz / sphere.radius);
let color: Vector3D =
lights.shade(position, normal, &sphere.material, &occluders);
sum_r += color.get_x();
sum_g += color.get_y();
sum_b += color.get_z();
}
if inside_count > 0 {
let inv_count: f64 = 1.0 / f64::from(inside_count);
red = sum_r * inv_count;
green = sum_g * inv_count;
blue = sum_b * inv_count;
alpha = 255;
}
}
buffer[index] = gamma_byte(red);
buffer[index + 1] = gamma_byte(green);
buffer[index + 2] = gamma_byte(blue);
buffer[index + 3] = alpha;
index += 4;
}
}
}
fn present_lighting_framebuffer(
context: &CanvasRenderingContext2d,
buffer: &mut [u8],
width: u32,
height: u32,
) {
let image_data: Result<ImageData, JsValue> =
ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(buffer), width, height);
if let Ok(image_data) = image_data {
let _: Result<(), JsValue> = context.put_image_data(&image_data, 0.0, 0.0);
}
}
pub(crate) fn start_lighting_loop(state: UseLighting) {
let raf_id: Rc<Cell<Option<i32>>> = Rc::new(Cell::new(None));
let closure_cell: RafClosureCell = Rc::new(MaybeEngineCell::new());
let last_time: Rc<Cell<f64>> = Rc::new(Cell::new(-1.0));
let frame_count: Rc<Cell<u32>> = Rc::new(Cell::new(0));
let fps_timer: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
let (spheres, lights) = build_lighting_scene();
let canvas_cache: Rc<RefCell<Option<(HtmlCanvasElement, CanvasRenderingContext2d)>>> =
Rc::new(RefCell::new(None));
let framebuffer: Rc<RefCell<Vec<u8>>> = Rc::new(RefCell::new(Vec::new()));
let scale_index: Rc<Cell<usize>> = Rc::new(Cell::new(7));
let ema_millis: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
let slow_frames: Rc<Cell<u32>> = Rc::new(Cell::new(0));
let fast_frames: Rc<Cell<u32>> = Rc::new(Cell::new(0));
let very_fast_frames: Rc<Cell<u32>> = Rc::new(Cell::new(0));
let last_clone: Rc<Cell<f64>> = last_time.clone();
let frame_clone: Rc<Cell<u32>> = frame_count.clone();
let fps_clone: Rc<Cell<f64>> = fps_timer.clone();
let raf_clone: Rc<Cell<Option<i32>>> = raf_id.clone();
let cell_clone: RafClosureCell = closure_cell.clone();
let cache_clone: Rc<RefCell<Option<(HtmlCanvasElement, CanvasRenderingContext2d)>>> =
canvas_cache.clone();
let buffer_clone: Rc<RefCell<Vec<u8>>> = framebuffer.clone();
let scale_clone: Rc<Cell<usize>> = scale_index.clone();
let ema_clone: Rc<Cell<f64>> = ema_millis.clone();
let slow_clone: Rc<Cell<u32>> = slow_frames.clone();
let fast_clone: Rc<Cell<u32>> = fast_frames.clone();
let very_fast_clone: Rc<Cell<u32>> = very_fast_frames.clone();
if let Some(loading_window) = window() {
let loading_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
draw_game_3d_loading(LIGHTING_LOADING_CANVAS_SELECTOR, LIGHTING_CANVAS_SELECTOR);
}));
let loading_callback: Function =
loading_closure.as_ref().unchecked_ref::<Function>().clone();
loading_closure.forget();
let _ = loading_window
.set_timeout_with_callback_and_timeout_and_arguments_0(&loading_callback, 0);
}
let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
if lighting_canvas_detached(LIGHTING_CANVAS_SELECTOR) {
return;
}
let Some(window_value): Option<Window> = window() else {
return;
};
let Some(performance): Option<Performance> = window_value.performance() else {
return;
};
let current_time: f64 = performance.now() / 1000.0;
let prev: f64 = last_clone.get();
let frame_time: f64 = if prev < 0.0 {
1.0 / 60.0
} else {
current_time - prev
};
last_clone.set(current_time);
if state.get_running().get() {
let scale: f64 = LIGHTING_RENDER_SCALES[scale_clone.get()];
let (frame_width, frame_height): (u32, u32) = lighting_scaled_dimensions(scale);
let mut cache = cache_clone.borrow_mut();
let cached_valid: bool = cache.as_ref().is_some_and(
|(canvas, _): &(HtmlCanvasElement, CanvasRenderingContext2d)| canvas.is_connected(),
);
if !cached_valid {
*cache = acquire_lighting_canvas(frame_width, frame_height);
}
if let Some((canvas, context)) = cache.as_ref() {
if canvas.width() != frame_width {
canvas.set_width(frame_width);
}
if canvas.height() != frame_height {
canvas.set_height(frame_height);
}
let render_start: f64 = performance.now();
{
let mut buffer = buffer_clone.borrow_mut();
let needed: usize = frame_width as usize * frame_height as usize * 4;
if buffer.len() != needed {
buffer.resize(needed, 0);
}
render_lighting_frame(
&mut buffer,
frame_width,
frame_height,
scale,
&spheres,
&lights,
);
present_lighting_framebuffer(context, &mut buffer, frame_width, frame_height);
}
let render_millis: f64 = performance.now() - render_start;
let ema_prev: f64 = ema_clone.get();
let ema: f64 = if ema_prev <= 0.0 {
render_millis
} else {
ema_prev * (1.0 - LIGHTING_ADAPT_EMA_ALPHA)
+ render_millis * LIGHTING_ADAPT_EMA_ALPHA
};
ema_clone.set(ema);
if ema > LIGHTING_ADAPT_SLOW_FRAME_MILLIS {
slow_clone.set(slow_clone.get() + 1);
fast_clone.set(0);
very_fast_clone.set(0);
} else if ema < LIGHTING_ADAPT_VERY_FAST_FRAME_MILLIS {
fast_clone.set(fast_clone.get() + 1);
very_fast_clone.set(very_fast_clone.get() + 1);
slow_clone.set(0);
} else if ema < LIGHTING_ADAPT_FAST_FRAME_MILLIS {
fast_clone.set(fast_clone.get() + 1);
slow_clone.set(0);
very_fast_clone.set(0);
} else {
slow_clone.set(0);
fast_clone.set(0);
very_fast_clone.set(0);
}
let index: usize = scale_clone.get();
let mut next: usize = index;
if slow_clone.get() >= LIGHTING_ADAPT_SLOW_FRAMES
&& index + 1 < LIGHTING_RENDER_SCALES.len()
{
next = index + 1;
} else if fast_clone.get() >= LIGHTING_ADAPT_FAST_FRAMES && index > 0 {
next = if very_fast_clone.get() >= LIGHTING_ADAPT_FAST_FRAMES {
index.saturating_sub(2)
} else {
index - 1
};
}
if next != index {
scale_clone.set(next);
slow_clone.set(0);
fast_clone.set(0);
very_fast_clone.set(0);
state.get_render_scale().set(LIGHTING_RENDER_SCALES[next]);
}
if !state.get_active().get() {
state.get_active().set(true);
lighting_set_loaded_delayed(state.get_loaded(), GAME_3D_LOADING_MIN_MILLIS);
}
}
}
frame_clone.set(frame_clone.get() + 1);
fps_clone.set(fps_clone.get() + frame_time);
if fps_clone.get() >= 1.0 {
let fps: f64 = f64::from(frame_clone.get()) / fps_clone.get();
state.get_fps().set(fps);
frame_clone.set(0);
fps_clone.set(0.0);
}
let Some(raf_closure_ref): Option<&'static Closure<dyn FnMut()>> = cell_clone.try_get()
else {
return;
};
let next_id: i32 = window_value
.request_animation_frame(raf_closure_ref.as_ref().unchecked_ref())
.unwrap_or_default();
raf_clone.set(Some(next_id));
}));
let _: Result<(), _> = closure_cell.try_set(raf_closure);
let start_timeout_id: Rc<Cell<Option<i32>>> = Rc::new(Cell::new(None));
let start_timeout_clone: Rc<Cell<Option<i32>>> = start_timeout_id.clone();
let raf_for_start: Rc<Cell<Option<i32>>> = raf_id.clone();
let cell_for_start: RafClosureCell = closure_cell.clone();
let start_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
let Some(start_window): Option<Window> = window() else {
return;
};
let Some(start_raf_ref): Option<&'static Closure<dyn FnMut()>> = cell_for_start.try_get()
else {
return;
};
let start_id: i32 = start_window
.request_animation_frame(start_raf_ref.as_ref().unchecked_ref())
.unwrap_or_default();
raf_for_start.set(Some(start_id));
}));
let start_callback: Function = start_closure.as_ref().unchecked_ref::<Function>().clone();
start_closure.forget();
let Some(start_window): Option<Window> = window() else {
return;
};
let timeout_id: i32 = start_window
.set_timeout_with_callback_and_timeout_and_arguments_0(
&start_callback,
LIGHTING_LOOP_START_DELAY_MILLIS,
)
.unwrap_or_default();
start_timeout_clone.set(Some(timeout_id));
let raf_for_cleanup: Rc<Cell<Option<i32>>> = raf_id.clone();
let cell_for_cleanup: RafClosureCell = closure_cell.clone();
App::use_cleanup(move || {
if let Some(cancel_id) = raf_for_cleanup.get() {
let Some(window_value): Option<Window> = window() else {
return;
};
let _ = window_value.cancel_animation_frame(cancel_id);
}
if let Some(timeout_id) = start_timeout_id.get() {
let Some(window_value): Option<Window> = window() else {
return;
};
window_value.clear_timeout_with_handle(timeout_id);
}
let _: Option<_> = cell_for_cleanup.try_take();
});
state.get_loop_started().set(true);
}
pub(crate) fn lighting_on_toggle_pause(running: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
let current: bool = running.get();
running.set(!current);
}))
}
fn pack_lighting_gpu_uniform(width: f64, height: f64, background: (f64, f64, f64)) -> Vec<f32> {
let (red, green, blue) = background;
vec![
width as f32,
height as f32,
0.0,
0.0,
red as f32,
green as f32,
blue as f32,
1.0,
]
}
fn lighting_set_loaded_delayed(loaded: Signal<bool>, millis: i32) {
let loaded_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
loaded.set(true);
}));
let loaded_callback: Function = loaded_closure.as_ref().unchecked_ref::<Function>().clone();
loaded_closure.forget();
let Some(loaded_window): Option<Window> = window() else {
return;
};
let _ = loaded_window
.set_timeout_with_callback_and_timeout_and_arguments_0(&loaded_callback, millis);
}
fn lighting_register_resize_debounce(
resize_dirty: Rc<Cell<bool>>,
resize_timer: Rc<Cell<Option<i32>>>,
) {
let resize_dirty_for_event: Rc<Cell<bool>> = resize_dirty.clone();
let resize_timer_for_event: Rc<Cell<Option<i32>>> = resize_timer.clone();
let debounce_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
resize_dirty_for_event.set(true);
}));
let debounce_callback: Function = debounce_closure
.as_ref()
.unchecked_ref::<Function>()
.clone();
debounce_closure.forget();
let Some(resize_window): Option<Window> = window() else {
return;
};
App::use_window_event("resize", move || {
let old_timer: Option<i32> = resize_timer_for_event.get();
if let Some(timer_id) = old_timer {
let Some(clear_window): Option<Window> = window() else {
return;
};
clear_window.clear_timeout_with_handle(timer_id);
}
let new_timer: i32 = resize_window
.set_timeout_with_callback_and_timeout_and_arguments_0(
&debounce_callback,
GAME_3D_RESIZE_DEBOUNCE_MILLIS,
)
.unwrap_or_default();
resize_timer_for_event.set(Some(new_timer));
});
}
pub(crate) fn start_lighting_webgl_loop(state: UseLightingWebGl) {
let init_state: UseLightingWebGl = state;
let loop_state: UseLightingWebGl = state;
let raf_id: Rc<Cell<Option<i32>>> = Rc::new(Cell::new(None));
let closure_cell: RafClosureCell = Rc::new(MaybeEngineCell::new());
let resize_dirty: Rc<Cell<bool>> = Rc::new(Cell::new(false));
let resize_timer: Rc<Cell<Option<i32>>> = Rc::new(Cell::new(None));
let renderer_rc: Rc<RefCell<Option<WebGlRenderer>>> = Rc::new(RefCell::new(None));
let cancelled: Rc<Cell<bool>> = Rc::new(Cell::new(false));
let observer_cell: Rc<RefCell<Option<ResizeObserver>>> = Rc::new(RefCell::new(None));
lighting_register_resize_debounce(resize_dirty.clone(), resize_timer.clone());
let raf_for_cleanup: Rc<Cell<Option<i32>>> = raf_id.clone();
let cell_for_cleanup: RafClosureCell = closure_cell.clone();
let renderer_for_cleanup: Rc<RefCell<Option<WebGlRenderer>>> = renderer_rc.clone();
let resize_timer_for_cleanup: Rc<Cell<Option<i32>>> = resize_timer.clone();
let cancelled_for_cleanup: Rc<Cell<bool>> = cancelled.clone();
let observer_for_cleanup: Rc<RefCell<Option<ResizeObserver>>> = observer_cell.clone();
App::use_cleanup(move || {
cancelled_for_cleanup.set(true);
if let Some(cancel_id) = raf_for_cleanup.get()
&& let Some(window_value) = window()
{
let _ = window_value.cancel_animation_frame(cancel_id);
}
if let Some(timer_id) = resize_timer_for_cleanup.get()
&& let Some(window_value) = window()
{
window_value.clear_timeout_with_handle(timer_id);
}
if let Some(observer) = observer_for_cleanup.borrow_mut().take() {
observer.disconnect();
}
let _: Option<_> = cell_for_cleanup.try_take();
let _: Option<WebGlRenderer> = renderer_for_cleanup.borrow_mut().take();
});
let cancelled_for_init: Rc<Cell<bool>> = cancelled.clone();
let Some(loading_window): Option<Window> = window() else {
return;
};
let loading_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
draw_game_3d_loading(
LIGHTING_WEBGL_LOADING_CANVAS_SELECTOR,
LIGHTING_WEBGL_CANVAS_SELECTOR,
);
}));
let loading_callback: Function = loading_closure.as_ref().unchecked_ref::<Function>().clone();
loading_closure.forget();
let _ =
loading_window.set_timeout_with_callback_and_timeout_and_arguments_0(&loading_callback, 0);
spawn_local(async move {
if cancelled_for_init.get() {
return;
}
let config: RenderConfig = RenderConfig::webgl(
LIGHTING_WEBGL_CANVAS_SELECTOR,
LIGHTING_WIDTH,
LIGHTING_HEIGHT,
);
let renderer: WebGlRenderer = match Engine::webgl_renderer(&config) {
Ok(value) => value,
Err(error) => {
Console::error(format!("[euv-engine][lighting] webgl init failed: {error}"));
init_state.get_init_error_code().set(error.code());
init_state.get_loaded().set(true);
return;
}
};
let program: WebGlProgram = match renderer
.create_program(LIGHTING_WEBGL_VERTEX_SHADER, LIGHTING_WEBGL_FRAGMENT_SHADER)
{
Ok(value) => value,
Err(error) => {
Console::error(format!(
"[euv-engine][lighting] webgl program failed: {error}"
));
init_state.get_init_error_code().set("WEBGL_PROGRAM_ERROR");
init_state.get_loaded().set(true);
return;
}
};
let params_location: Rc<Option<WebGlUniformLocation>> =
Rc::new(renderer.get_uniform_location(&program, "u_params[0]"));
init_state.get_active().set(true);
lighting_set_loaded_delayed(init_state.get_loaded(), GAME_3D_LOADING_MIN_MILLIS);
*renderer_rc.borrow_mut() = Some(renderer);
let program_rc: Rc<WebGlProgram> = Rc::new(program);
let renderer_for_observer: Rc<RefCell<Option<WebGlRenderer>>> = renderer_rc.clone();
let observer_closure: Closure<dyn FnMut(js_sys::Array, ResizeObserver)> = Closure::wrap(
Box::new(move |_entries: js_sys::Array, _obs: ResizeObserver| {
let Some(window_value): Option<Window> = window() else {
return;
};
let Some(document_value): Option<Document> = window_value.document() else {
return;
};
let Some(element): Option<Element> = document_value
.query_selector(LIGHTING_WEBGL_CANVAS_SELECTOR)
.ok()
.flatten()
else {
return;
};
let canvas: HtmlCanvasElement = element.unchecked_into();
let rect: DomRect = canvas.get_bounding_client_rect();
let css_w: f64 = rect.width();
let css_h: f64 = rect.height();
if css_w <= 0.0 || css_h <= 0.0 {
return;
}
let dpr: f64 = Reflect::get(
window_value.as_ref(),
&JsValue::from_str("devicePixelRatio"),
)
.ok()
.and_then(|v: JsValue| v.as_f64())
.filter(|v: &f64| v.is_finite() && *v >= 1.0)
.unwrap_or(1.0);
let new_w: u32 = (css_w * dpr).round() as u32;
let new_h: u32 = (css_h * dpr).round() as u32;
let backing_w: u32 = canvas.width();
let backing_h: u32 = canvas.height();
if backing_w != new_w || backing_h != new_h {
canvas.set_width(new_w);
canvas.set_height(new_h);
if let Some(renderer) = renderer_for_observer.borrow_mut().as_mut() {
renderer.resize(new_w, new_h);
}
}
}),
);
let observer_callback: Function = observer_closure
.as_ref()
.unchecked_ref::<Function>()
.clone();
observer_closure.forget();
if let Ok(resize_observer) = ResizeObserver::new(&observer_callback)
&& let Some(window_value) = window()
&& let Some(document_value) = window_value.document()
&& let Some(element) = document_value
.query_selector(LIGHTING_WEBGL_CANVAS_SELECTOR)
.ok()
.flatten()
{
resize_observer.observe(&element);
*observer_cell.borrow_mut() = Some(resize_observer);
}
let last_time: Rc<Cell<f64>> = Rc::new(Cell::new(-1.0));
let frame_count: Rc<Cell<u32>> = Rc::new(Cell::new(0));
let fps_timer: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
let renderer_for_loop: Rc<RefCell<Option<WebGlRenderer>>> = renderer_rc.clone();
let program_for_loop: Rc<WebGlProgram> = program_rc.clone();
let params_location_for_loop: Rc<Option<WebGlUniformLocation>> = params_location.clone();
let raf_clone: Rc<Cell<Option<i32>>> = raf_id.clone();
let cell_clone: RafClosureCell = closure_cell.clone();
let last_clone: Rc<Cell<f64>> = last_time.clone();
let frame_clone: Rc<Cell<u32>> = frame_count.clone();
let fps_clone: Rc<Cell<f64>> = fps_timer.clone();
let resize_dirty_for_loop: Rc<Cell<bool>> = resize_dirty.clone();
let cancelled_for_loop: Rc<Cell<bool>> = cancelled.clone();
let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
if cancelled_for_loop.get() || lighting_canvas_detached(LIGHTING_WEBGL_CANVAS_SELECTOR)
{
return;
}
let Some(window_value): Option<Window> = window() else {
return;
};
let Some(performance): Option<Performance> = window_value.performance() else {
return;
};
let current_time: f64 = performance.now() / 1000.0;
let prev: f64 = last_clone.get();
let frame_time: f64 = if prev < 0.0 {
1.0 / 60.0
} else {
current_time - prev
};
last_clone.set(current_time);
let resize_dirty_frame: bool = if resize_dirty_for_loop.get() {
resize_dirty_for_loop.set(false);
true
} else {
false
};
let Some(window_for_dpr): Option<Window> = window() else {
return;
};
let dpr: f64 = Reflect::get(
window_for_dpr.as_ref(),
&JsValue::from_str("devicePixelRatio"),
)
.ok()
.and_then(|value: JsValue| value.as_f64())
.filter(|value: &f64| value.is_finite() && *value >= 1.0)
.unwrap_or(1.0);
let (canvas_width, canvas_height): (f64, f64) =
read_lighting_canvas_size(LIGHTING_WEBGL_CANVAS_SELECTOR).unwrap_or((0.0, 0.0));
let new_physical_width: u32 = (canvas_width * dpr).round() as u32;
let new_physical_height: u32 = (canvas_height * dpr).round() as u32;
if let Some(renderer) = renderer_for_loop.borrow_mut().as_mut() {
if new_physical_width > 0 && new_physical_height > 0 {
let backing_w: u32 = renderer.get_canvas().width();
let backing_h: u32 = renderer.get_canvas().height();
if backing_w != new_physical_width || backing_h != new_physical_height {
renderer.get_canvas().set_width(new_physical_width);
renderer.get_canvas().set_height(new_physical_height);
renderer.resize(new_physical_width, new_physical_height);
}
}
if resize_dirty_frame {
renderer.resize(new_physical_width, new_physical_height);
}
if loop_state.get_running().get() {
let backing_w: f64 = f64::from(renderer.get_canvas().width());
let backing_h: f64 = f64::from(renderer.get_canvas().height());
let background: (f64, f64, f64) =
game_3d_canvas_clear_color(LIGHTING_WEBGL_CANVAS_SELECTOR);
let uniform_data: Vec<f32> =
pack_lighting_gpu_uniform(backing_w, backing_h, background);
renderer.set_uniform_4fv(
&program_for_loop,
params_location_for_loop.as_ref().as_ref(),
&uniform_data,
);
renderer.render_frame(
&program_for_loop,
(background.0, background.1, background.2, 1.0),
3,
);
}
}
frame_clone.set(frame_clone.get() + 1);
fps_clone.set(fps_clone.get() + frame_time);
if fps_clone.get() >= 1.0 {
let fps: f64 = f64::from(frame_clone.get()) / fps_clone.get();
loop_state.get_fps().set(fps);
frame_clone.set(0);
fps_clone.set(0.0);
}
let Some(raf_closure_ref): Option<&'static Closure<dyn FnMut()>> = cell_clone.try_get()
else {
return;
};
let next_id: i32 = window_value
.request_animation_frame(raf_closure_ref.as_ref().unchecked_ref())
.unwrap_or_default();
if cancelled_for_loop.get() {
raf_clone.set(None);
} else {
raf_clone.set(Some(next_id));
}
}));
let _: Result<(), _> = closure_cell.try_set(raf_closure);
let Some(start_window): Option<Window> = window() else {
return;
};
let Some(start_raf_ref): Option<&'static Closure<dyn FnMut()>> = closure_cell.try_get()
else {
return;
};
let start_id: i32 = start_window
.request_animation_frame(start_raf_ref.as_ref().unchecked_ref())
.unwrap_or_default();
raf_id.set(Some(start_id));
});
}
pub(crate) fn start_lighting_webgpu_loop(state: UseLightingWebGpu) {
let init_state: UseLightingWebGpu = state;
let loop_state: UseLightingWebGpu = state;
let raf_id: Rc<Cell<Option<i32>>> = Rc::new(Cell::new(None));
let closure_cell: RafClosureCell = Rc::new(MaybeEngineCell::new());
let resize_dirty: Rc<Cell<bool>> = Rc::new(Cell::new(false));
let resize_timer: Rc<Cell<Option<i32>>> = Rc::new(Cell::new(None));
let renderer_rc: Rc<RefCell<Option<WebGpuRenderer>>> = Rc::new(RefCell::new(None));
let cancelled: Rc<Cell<bool>> = Rc::new(Cell::new(false));
let observer_cell: Rc<RefCell<Option<ResizeObserver>>> = Rc::new(RefCell::new(None));
lighting_register_resize_debounce(resize_dirty.clone(), resize_timer.clone());
let raf_for_cleanup: Rc<Cell<Option<i32>>> = raf_id.clone();
let cell_for_cleanup: RafClosureCell = closure_cell.clone();
let renderer_for_cleanup: Rc<RefCell<Option<WebGpuRenderer>>> = renderer_rc.clone();
let resize_timer_for_cleanup: Rc<Cell<Option<i32>>> = resize_timer.clone();
let cancelled_for_cleanup: Rc<Cell<bool>> = cancelled.clone();
let observer_for_cleanup: Rc<RefCell<Option<ResizeObserver>>> = observer_cell.clone();
App::use_cleanup(move || {
cancelled_for_cleanup.set(true);
if let Some(cancel_id) = raf_for_cleanup.get()
&& let Some(window_value) = window()
{
let _ = window_value.cancel_animation_frame(cancel_id);
}
if let Some(timer_id) = resize_timer_for_cleanup.get()
&& let Some(window_value) = window()
{
window_value.clear_timeout_with_handle(timer_id);
}
if let Some(observer) = observer_for_cleanup.borrow_mut().take() {
observer.disconnect();
}
let _: Option<_> = cell_for_cleanup.try_take();
if let Some(renderer) = renderer_for_cleanup.borrow_mut().take() {
renderer.dispose();
}
});
let cancelled_for_init: Rc<Cell<bool>> = cancelled.clone();
let Some(loading_window): Option<Window> = window() else {
return;
};
let loading_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
draw_game_3d_loading(
LIGHTING_WEBGPU_LOADING_CANVAS_SELECTOR,
LIGHTING_WEBGPU_CANVAS_SELECTOR,
);
}));
let loading_callback: Function = loading_closure.as_ref().unchecked_ref::<Function>().clone();
loading_closure.forget();
let _ =
loading_window.set_timeout_with_callback_and_timeout_and_arguments_0(&loading_callback, 0);
spawn_local(async move {
let config: RenderConfig = RenderConfig::webgpu(
LIGHTING_WEBGPU_CANVAS_SELECTOR,
LIGHTING_WIDTH,
LIGHTING_HEIGHT,
);
let renderer: Result<WebGpuRenderer, WebGpuInitError> =
Engine::webgpu_renderer(&config).await;
if cancelled_for_init.get() {
if let Ok(stale_renderer) = &renderer {
stale_renderer.dispose();
}
return;
}
let renderer: WebGpuRenderer = match renderer {
Ok(value) => value,
Err(error) => {
Console::error(format!(
"[euv-engine][lighting] webgpu init failed: {error}"
));
init_state.get_init_error_code().set(error.code());
init_state.get_loaded().set(true);
return;
}
};
let pipeline: JsValue = renderer.create_render_pipeline(LIGHTING_WEBGPU_SHADER);
let uniform_buffer: JsValue =
renderer.create_uniform_buffer(&[0.0; LIGHTING_GPU_UNIFORM_VEC4_COUNT * 4]);
let bind_group: JsValue = renderer.create_uniform_bind_group(&pipeline, &uniform_buffer);
init_state.get_active().set(true);
lighting_set_loaded_delayed(init_state.get_loaded(), GAME_3D_LOADING_MIN_MILLIS);
*renderer_rc.borrow_mut() = Some(renderer);
let pipeline_rc: Rc<JsValue> = Rc::new(pipeline);
let buffer_rc: Rc<JsValue> = Rc::new(uniform_buffer);
let bind_group_rc: Rc<JsValue> = Rc::new(bind_group);
let renderer_for_observer: Rc<RefCell<Option<WebGpuRenderer>>> = renderer_rc.clone();
let observer_closure: Closure<dyn FnMut(js_sys::Array, ResizeObserver)> = Closure::wrap(
Box::new(move |_entries: js_sys::Array, _obs: ResizeObserver| {
let Some(window_value): Option<Window> = window() else {
return;
};
let Some(document_value): Option<Document> = window_value.document() else {
return;
};
let Some(element): Option<Element> = document_value
.query_selector(LIGHTING_WEBGPU_CANVAS_SELECTOR)
.ok()
.flatten()
else {
return;
};
let canvas: HtmlCanvasElement = element.unchecked_into();
let rect: DomRect = canvas.get_bounding_client_rect();
let css_w: f64 = rect.width();
let css_h: f64 = rect.height();
if css_w <= 0.0 || css_h <= 0.0 {
return;
}
let dpr: f64 = Reflect::get(
window_value.as_ref(),
&JsValue::from_str("devicePixelRatio"),
)
.ok()
.and_then(|v: JsValue| v.as_f64())
.filter(|v: &f64| v.is_finite() && *v >= 1.0)
.unwrap_or(1.0);
let new_w: u32 = (css_w * dpr).round() as u32;
let new_h: u32 = (css_h * dpr).round() as u32;
let backing_w: u32 = canvas.width();
let backing_h: u32 = canvas.height();
if backing_w != new_w || backing_h != new_h {
canvas.set_width(new_w);
canvas.set_height(new_h);
if let Some(renderer) = renderer_for_observer.borrow_mut().as_mut() {
renderer.resize(new_w, new_h);
}
}
}),
);
let observer_callback: Function = observer_closure
.as_ref()
.unchecked_ref::<Function>()
.clone();
observer_closure.forget();
if let Ok(resize_observer) = ResizeObserver::new(&observer_callback)
&& let Some(window_value) = window()
&& let Some(document_value) = window_value.document()
&& let Some(element) = document_value
.query_selector(LIGHTING_WEBGPU_CANVAS_SELECTOR)
.ok()
.flatten()
{
resize_observer.observe(&element);
*observer_cell.borrow_mut() = Some(resize_observer);
}
let last_time: Rc<Cell<f64>> = Rc::new(Cell::new(-1.0));
let frame_count: Rc<Cell<u32>> = Rc::new(Cell::new(0));
let fps_timer: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
let renderer_for_loop: Rc<RefCell<Option<WebGpuRenderer>>> = renderer_rc.clone();
let pipeline_for_loop: Rc<JsValue> = pipeline_rc.clone();
let buffer_for_loop: Rc<JsValue> = buffer_rc.clone();
let bind_group_for_loop: Rc<JsValue> = bind_group_rc.clone();
let raf_clone: Rc<Cell<Option<i32>>> = raf_id.clone();
let cell_clone: RafClosureCell = closure_cell.clone();
let last_clone: Rc<Cell<f64>> = last_time.clone();
let frame_clone: Rc<Cell<u32>> = frame_count.clone();
let fps_clone: Rc<Cell<f64>> = fps_timer.clone();
let resize_dirty_for_loop: Rc<Cell<bool>> = resize_dirty.clone();
let cancelled_for_loop: Rc<Cell<bool>> = cancelled.clone();
let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
if cancelled_for_loop.get() || lighting_canvas_detached(LIGHTING_WEBGPU_CANVAS_SELECTOR)
{
return;
}
let Some(window_value): Option<Window> = window() else {
return;
};
let Some(performance): Option<Performance> = window_value.performance() else {
return;
};
let current_time: f64 = performance.now() / 1000.0;
let prev: f64 = last_clone.get();
let frame_time: f64 = if prev < 0.0 {
1.0 / 60.0
} else {
current_time - prev
};
last_clone.set(current_time);
let resize_dirty_frame: bool = if resize_dirty_for_loop.get() {
resize_dirty_for_loop.set(false);
true
} else {
false
};
let Some(window_for_dpr): Option<Window> = window() else {
return;
};
let dpr: f64 = Reflect::get(
window_for_dpr.as_ref(),
&JsValue::from_str("devicePixelRatio"),
)
.ok()
.and_then(|value: JsValue| value.as_f64())
.filter(|value: &f64| value.is_finite() && *value >= 1.0)
.unwrap_or(1.0);
let (canvas_width, canvas_height): (f64, f64) =
read_lighting_canvas_size(LIGHTING_WEBGPU_CANVAS_SELECTOR).unwrap_or((0.0, 0.0));
let new_physical_width: u32 = (canvas_width * dpr).round() as u32;
let new_physical_height: u32 = (canvas_height * dpr).round() as u32;
if let Some(renderer) = renderer_for_loop.borrow_mut().as_mut() {
if new_physical_width > 0 && new_physical_height > 0 {
let backing_w: u32 = renderer.get_canvas().width();
let backing_h: u32 = renderer.get_canvas().height();
if backing_w != new_physical_width || backing_h != new_physical_height {
renderer.get_canvas().set_width(new_physical_width);
renderer.get_canvas().set_height(new_physical_height);
let _ = renderer.resize(new_physical_width, new_physical_height);
}
}
if resize_dirty_frame {
let _ = renderer.resize(new_physical_width, new_physical_height);
}
if loop_state.get_running().get() {
let backing_w: f64 = f64::from(renderer.get_canvas().width());
let backing_h: f64 = f64::from(renderer.get_canvas().height());
let background: (f64, f64, f64) =
game_3d_canvas_clear_color(LIGHTING_WEBGPU_CANVAS_SELECTOR);
let uniform_data: Vec<f32> =
pack_lighting_gpu_uniform(backing_w, backing_h, background);
renderer.update_uniform_buffer(&buffer_for_loop, &uniform_data);
renderer.render_frame_with_bind_group(
&pipeline_for_loop,
&bind_group_for_loop,
(background.0, background.1, background.2, 1.0),
3,
);
}
}
frame_clone.set(frame_clone.get() + 1);
fps_clone.set(fps_clone.get() + frame_time);
if fps_clone.get() >= 1.0 {
let fps: f64 = f64::from(frame_clone.get()) / fps_clone.get();
loop_state.get_fps().set(fps);
frame_clone.set(0);
fps_clone.set(0.0);
}
let Some(raf_closure_ref): Option<&'static Closure<dyn FnMut()>> = cell_clone.try_get()
else {
return;
};
let next_id: i32 = window_value
.request_animation_frame(raf_closure_ref.as_ref().unchecked_ref())
.unwrap_or_default();
if cancelled_for_loop.get() {
raf_clone.set(None);
} else {
raf_clone.set(Some(next_id));
}
}));
let _: Result<(), _> = closure_cell.try_set(raf_closure);
let Some(start_window): Option<Window> = window() else {
return;
};
let Some(start_raf_ref): Option<&'static Closure<dyn FnMut()>> = closure_cell.try_get()
else {
return;
};
let start_id: i32 = start_window
.request_animation_frame(start_raf_ref.as_ref().unchecked_ref())
.unwrap_or_default();
raf_id.set(Some(start_id));
});
}
pub(crate) fn lighting_on_tab_select(
tab: Signal<LightingTab>,
value: LightingTab,
fullscreen: UseLightingFullscreen,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
fullscreen.get_canvas_2d().set(false);
fullscreen.get_web_gl().set(false);
fullscreen.get_web_gpu().set(false);
tab.set(value);
}))
}
pub(crate) fn enter_lighting_fullscreen(tab: Signal<bool>) {
tab.set(true);
Router::overlay_push_state();
UseEuvLayout::apply_cached_insets();
let Some(window_value): Option<Window> = window() else {
return;
};
let event: Result<Event, JsValue> = Event::new("resize");
if let Ok(event) = event {
let _ = window_value.dispatch_event(&event);
}
}
pub(crate) fn exit_lighting_fullscreen(tab: Signal<bool>) {
tab.set(false);
UseEuvLayout::apply_cached_insets();
let Some(window_value): Option<Window> = window() else {
return;
};
let event: Result<Event, JsValue> = Event::new("resize");
if let Ok(event) = event {
let _ = window_value.dispatch_event(&event);
}
}
pub(crate) fn exit_lighting_fullscreen_from_popstate(tab: Signal<bool>) {
tab.set(false);
UseEuvLayout::apply_cached_insets();
let Some(window_value): Option<Window> = window() else {
return;
};
let event: Result<Event, JsValue> = Event::new("resize");
if let Ok(event) = event {
let _ = window_value.dispatch_event(&event);
}
}
pub(crate) fn use_lighting_fullscreen_popstate(state: UseLightingFullscreen) -> usize {
Router::register_popstate_guard(Rc::new(move || {
if state.get_canvas_2d().get() {
exit_lighting_fullscreen_from_popstate(state.get_canvas_2d());
true
} else if state.get_web_gl().get() {
exit_lighting_fullscreen_from_popstate(state.get_web_gl());
true
} else if state.get_web_gpu().get() {
exit_lighting_fullscreen_from_popstate(state.get_web_gpu());
true
} else {
false
}
}))
}
pub(crate) fn lighting_on_enter_fullscreen(tab: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
enter_lighting_fullscreen(tab);
}))
}
pub(crate) fn lighting_on_exit_fullscreen(tab: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
exit_lighting_fullscreen(tab);
Router::overlay_back(None);
}))
}