use super::*;
pub(crate) fn use_raytrace_state() -> UseRayTrace {
UseRayTrace {
fps: App::use_signal(|| 0.0),
running: App::use_signal(|| true),
loop_started: App::use_signal(|| false),
auto_rotate: App::use_signal(|| true),
render_scale: App::use_signal(|| 1.0),
loaded: App::use_signal(|| false),
active: App::use_signal(|| false),
}
}
pub(crate) fn use_raytrace_webgl_state() -> UseRayTraceWebGl {
UseRayTraceWebGl {
fps: App::use_signal(|| 0.0),
running: App::use_signal(|| true),
auto_rotate: 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_raytrace_webgpu_state() -> UseRayTraceWebGpu {
UseRayTraceWebGpu {
fps: App::use_signal(|| 0.0),
running: App::use_signal(|| true),
auto_rotate: 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_raytrace_fullscreen_state() -> UseRayTraceFullscreen {
UseRayTraceFullscreen {
canvas_2d: App::use_signal(|| false),
web_gl: App::use_signal(|| false),
web_gpu: App::use_signal(|| false),
}
}
fn raytrace_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_raytrace_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_raytrace_ssaa_canvas() -> Option<(HtmlCanvasElement, SsaaCanvas)> {
let window_value: Window = window()?;
let is_mobile: bool = window_value
.inner_width()
.ok()
.and_then(|value: JsValue| value.as_f64())
.is_some_and(|width: f64| width < 768.0);
let scale_factor: f64 = if is_mobile { 1.0 } else { 2.0 };
let (canvas_width, canvas_height): (f64, f64) =
read_raytrace_canvas_size(RAYTRACE_CANVAS_SELECTOR)?;
let ssaa_canvas: SsaaCanvas = SsaaCanvas::from_selector_with_scale(
RAYTRACE_CANVAS_SELECTOR,
canvas_width,
canvas_height,
scale_factor,
)?;
let document_value: Document = window_value.document()?;
let element: Element = document_value
.query_selector(RAYTRACE_CANVAS_SELECTOR)
.ok()
.flatten()?;
let display_canvas: HtmlCanvasElement = element.unchecked_into();
Some((display_canvas, ssaa_canvas))
}
fn build_raytrace_scene(yaw: f64) -> (Vec<Occluder>, Vector3D) {
let ground_min: Vector3D = Vector3D::new(-5.0, -0.6, -5.0);
let ground_max: Vector3D = Vector3D::new(5.0, -0.5, 5.0);
let ground_material: Material = Material::phong(Vector3D::new(0.30, 0.32, 0.36), 0.30, 24.0);
let ground: Occluder = Occluder::aabb(ground_min, ground_max, ground_material);
let mirror_material: Material = Material::phong(Vector3D::new(0.05, 0.05, 0.06), 1.0, 64.0);
let mirror: Occluder = Occluder::sphere(Vector3D::new(0.0, 0.4, 0.0), 0.9, mirror_material);
let emissive_material: Material = Material::emissive(Vector3D::new(1.0, 0.45, 0.10));
let emissive: Occluder =
Occluder::sphere(Vector3D::new(1.6, 0.6, -1.4), 0.45, emissive_material);
let sun_material: Material = Material::emissive(Vector3D::new(1.00, 0.95, 0.85));
let sun: Occluder = Occluder::sphere(
raytrace_sun_position(yaw),
RAYTRACE_SUN_RADIUS,
sun_material,
);
let occluders: Vec<Occluder> = vec![ground, mirror, emissive, sun];
let eye: Vector3D = Vector3D::new(0.0, 0.8, 3.5);
(occluders, eye)
}
fn raytrace_sun_direction(yaw: f64) -> Vector3D {
Vector3D::new(-yaw.cos(), -0.5, -yaw.sin()).normalized()
}
fn raytrace_sun_position(yaw: f64) -> Vector3D {
let direction: Vector3D = raytrace_sun_direction(yaw);
Vector3D::new(direction.get_x(), -direction.get_y(), direction.get_z())
.scaled(RAYTRACE_SUN_DISTANCE)
}
fn build_raytrace_lighting(eye: Vector3D, yaw: f64) -> LightingUniforms {
let sun: Light =
Light::new_directional(raytrace_sun_direction(yaw), Vector3D::new(1.0, 0.95, 0.85));
let mut lights: LightingUniforms = LightingUniforms::with_eye(eye);
lights.set_ambient(Vector3D::new(0.10, 0.10, 0.14));
lights.add_light(sun);
lights
}
fn clamp_unit(value: f64) -> f64 {
value.clamp(0.0, 1.0)
}
fn gamma_byte(value: f64) -> u8 {
(clamp_unit(value).powf(1.0 / 2.2) * 255.0).round() as u8
}
fn build_camera_basis(eye: Vector3D, yaw: f64, pitch: f64) -> (Vector3D, Vector3D, Vector3D) {
let look_at: Vector3D = Vector3D::new(0.0, 0.4, 0.0);
let up: Vector3D = Vector3D::new(0.0, 1.0, 0.0);
let forward: Vector3D = (look_at - eye).normalized();
let _ = yaw;
let _ = pitch;
let right: Vector3D = forward.cross(up).normalized();
let up_true: Vector3D = right.cross(forward).normalized();
(forward, right, up_true)
}
fn compute_eye_position(yaw: f64, pitch: f64) -> Vector3D {
let cos_pitch: f64 = pitch.cos();
let distance: f64 = RAYTRACE_CAMERA_DISTANCE;
Vector3D::new(
distance * yaw.sin() * cos_pitch,
RAYTRACE_CAMERA_LOOK_AT_Y + distance * pitch.sin(),
RAYTRACE_CAMERA_LOOK_AT_Z + distance * yaw.cos() * cos_pitch,
)
}
fn raytrace_scaled_dimensions(scale: f64) -> (u32, u32) {
let width: u32 = (RAYTRACE_WIDTH * scale).round() as u32;
let height: u32 = (RAYTRACE_HEIGHT * scale).round() as u32;
(width, height)
}
fn render_raytrace_frame(
buffer: &mut [u8],
width: u32,
height: u32,
scene: &RayTraceScene,
lights: &LightingUniforms,
yaw: f64,
pitch: f64,
) {
let width_f64: f64 = f64::from(width);
let height_f64: f64 = f64::from(height);
let eye: Vector3D = compute_eye_position(yaw, pitch);
let (forward, right, up_true): (Vector3D, Vector3D, Vector3D) =
build_camera_basis(eye, yaw, pitch);
let aspect: f64 = width_f64 / height_f64;
let focal: f64 = 1.0;
let inv_width: f64 = 1.0 / width_f64;
let inv_height: f64 = 1.0 / height_f64;
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 acc_r: f64 = 0.0;
let mut acc_g: f64 = 0.0;
let mut acc_b: f64 = 0.0;
for (dx_off, dy_off) in sub_offsets {
let px: f64 = f64::from(x) + dx_off;
let py: f64 = f64::from(y) + dy_off;
let ndc_x: f64 = (px * inv_width) * 2.0 - 1.0;
let ndc_y: f64 = 1.0 - (py * inv_height) * 2.0;
let dir: Vector3D =
(forward.scaled(focal) + right.scaled(ndc_x * aspect) + up_true.scaled(ndc_y))
.normalized();
let ray: Ray = Ray::new(eye, dir);
let color: Vector3D = scene.trace(ray, lights);
acc_r += color.get_x();
acc_g += color.get_y();
acc_b += color.get_z();
}
buffer[index] = gamma_byte(acc_r * 0.25);
buffer[index + 1] = gamma_byte(acc_g * 0.25);
buffer[index + 2] = gamma_byte(acc_b * 0.25);
buffer[index + 3] = 255;
index += 4;
}
}
}
fn present_raytrace_framebuffer(
ssaa_canvas: &SsaaCanvas,
buffer: &mut [u8],
width: u32,
height: u32,
) {
let Some(window_value): Option<Window> = window() else {
return;
};
let Some(document_value): Option<Document> = window_value.document() else {
return;
};
let Ok(source_canvas): Result<HtmlCanvasElement, JsValue> = document_value
.create_element("canvas")
.map(|element: Element| element.unchecked_into())
else {
return;
};
source_canvas.set_width(width);
source_canvas.set_height(height);
let Ok(Some(source_context_object)): Result<Option<Object>, JsValue> =
source_canvas.get_context(RAYTRACE_CONTEXT_TYPE)
else {
return;
};
let source_context: CanvasRenderingContext2d = source_context_object.unchecked_into();
let image_data: Result<ImageData, JsValue> =
ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(buffer), width, height);
let Ok(image_data) = image_data else {
return;
};
let _: Result<(), JsValue> = source_context.put_image_data(&image_data, 0.0, 0.0);
let offscreen_context: &CanvasRenderingContext2d = ssaa_canvas.get_offscreen_context();
let offscreen_width: f64 = ssaa_canvas.get_width();
let offscreen_height: f64 = ssaa_canvas.get_height();
let dest_w: f64 = offscreen_width.min(offscreen_height * 4.0 / 3.0);
let dest_h: f64 = offscreen_height.min(offscreen_width * 3.0 / 4.0);
let dest_x: f64 = (offscreen_width - dest_w) * 0.5;
let dest_y: f64 = (offscreen_height - dest_h) * 0.5;
let _: Result<(), JsValue> = offscreen_context
.draw_image_with_html_canvas_element_and_dw_and_dh(
&source_canvas,
dest_x,
dest_y,
dest_w,
dest_h,
);
ssaa_canvas.present();
}
pub(crate) fn start_raytrace_loop(state: UseRayTrace, angles: RayTraceCameraAngles) {
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 (_, eye): (Vec<Occluder>, Vector3D) = build_raytrace_scene(0.0);
let ssaa_cache: Rc<RefCell<Option<(HtmlCanvasElement, SsaaCanvas)>>> =
Rc::new(RefCell::new(None));
let last_canvas_size: Rc<RefCell<(f64, f64)>> = Rc::new(RefCell::new((0.0, 0.0)));
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 yaw_clone: Rc<Cell<f64>> = angles.yaw.clone();
let pitch_clone: Rc<Cell<f64>> = angles.pitch.clone();
let cache_clone: Rc<RefCell<Option<(HtmlCanvasElement, SsaaCanvas)>>> = ssaa_cache.clone();
let last_size_clone: Rc<RefCell<(f64, f64)>> = last_canvas_size.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();
let state_for_loop: UseRayTrace = state;
if let Some(window_value) = window() {
let loading_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
draw_game_3d_loading(RAYTRACE_LOADING_CANVAS_SELECTOR, RAYTRACE_CANVAS_SELECTOR);
}));
let loading_callback: Function =
loading_closure.as_ref().unchecked_ref::<Function>().clone();
loading_closure.forget();
let _ = window_value
.set_timeout_with_callback_and_timeout_and_arguments_0(&loading_callback, 0);
}
let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
if raytrace_canvas_detached(RAYTRACE_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
};
let anim_time: f64 = frame_time.min(0.25);
last_clone.set(current_time);
if state_for_loop.get_auto_rotate().get() {
yaw_clone.set(yaw_clone.get() + RAYTRACE_AUTO_YAW_SPEED * anim_time);
}
let yaw: f64 = yaw_clone.get();
let pitch: f64 = pitch_clone.get();
if state_for_loop.get_running().get() {
let (live_w, live_h): (f64, f64) =
read_raytrace_canvas_size(RAYTRACE_CANVAS_SELECTOR).unwrap_or((0.0, 0.0));
let (cached_w, cached_h) = *last_size_clone.borrow();
let needs_reacquire: bool = live_w > 0.0
&& live_h > 0.0
&& (cached_w <= 0.0
|| cached_h <= 0.0
|| (live_w - cached_w).abs() > 1.5
|| (live_h - cached_h).abs() > 1.5);
if needs_reacquire {
if let Some((_, ssaa_canvas)) = acquire_raytrace_ssaa_canvas() {
*cache_clone.borrow_mut() =
Some((ssaa_canvas.get_display_canvas().clone(), ssaa_canvas));
}
*last_size_clone.borrow_mut() = (live_w, live_h);
}
let scale: f64 = RAYTRACE_RENDER_SCALES[scale_clone.get()];
let (frame_width, frame_height): (u32, u32) = raytrace_scaled_dimensions(scale);
if let Some((_canvas, ssaa_canvas)) = cache_clone.borrow().as_ref() {
let (occluders, _): (Vec<Occluder>, Vector3D) = build_raytrace_scene(yaw);
let scene: RayTraceScene = RayTraceScene::new(occluders);
let lights: LightingUniforms = build_raytrace_lighting(eye, yaw);
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_raytrace_frame(
&mut buffer,
frame_width,
frame_height,
&scene,
&lights,
yaw,
pitch,
);
present_raytrace_framebuffer(
ssaa_canvas,
&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 - RAYTRACE_ADAPT_EMA_ALPHA)
+ render_millis * RAYTRACE_ADAPT_EMA_ALPHA
};
ema_clone.set(ema);
if ema > RAYTRACE_ADAPT_SLOW_FRAME_MILLIS {
slow_clone.set(slow_clone.get() + 1);
fast_clone.set(0);
very_fast_clone.set(0);
} else if ema < RAYTRACE_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 < RAYTRACE_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() >= RAYTRACE_ADAPT_SLOW_FRAMES
&& index + 1 < RAYTRACE_RENDER_SCALES.len()
{
next = index + 1;
} else if fast_clone.get() >= RAYTRACE_ADAPT_FAST_FRAMES && index > 0 {
next = if very_fast_clone.get() >= RAYTRACE_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_for_loop
.get_render_scale()
.set(RAYTRACE_RENDER_SCALES[next]);
}
if !state_for_loop.get_active().get() {
state_for_loop.get_active().set(true);
raytrace_set_loaded_delayed_canvas2d(
state_for_loop.get_loaded(),
RAYTRACE_CANVAS_2D_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_for_loop.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,
RAYTRACE_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 raytrace_on_toggle_pause(running: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
let current: bool = running.get();
running.set(!current);
}))
}
pub(crate) fn raytrace_on_toggle_auto_rotate(
auto_rotate: Signal<bool>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
let current: bool = auto_rotate.get();
auto_rotate.set(!current);
}))
}
pub(crate) fn raytrace_on_reset_camera(angles: RayTraceCameraAngles) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
angles.yaw.set(0.6);
angles.pitch.set(0.25);
}))
}
fn client_x_from_event(event: &Event) -> f64 {
Reflect::get(event.as_ref(), &JsValue::from_str("clientX"))
.ok()
.and_then(|value: JsValue| value.as_f64())
.unwrap_or_default()
}
fn client_y_from_event(event: &Event) -> f64 {
Reflect::get(event.as_ref(), &JsValue::from_str("clientY"))
.ok()
.and_then(|value: JsValue| value.as_f64())
.unwrap_or_default()
}
pub(crate) fn raytrace_on_pointer_move(
angles: RayTraceCameraAngles,
auto_rotate: Signal<bool>,
last_pointer: Rc<Cell<Option<(f64, f64)>>>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
let last: Option<(f64, f64)> = last_pointer.get();
let Some((last_x, last_y)) = last else {
return;
};
let client_x: f64 = client_x_from_event(&event);
let client_y: f64 = client_y_from_event(&event);
let dx: f64 = client_x - last_x;
let dy: f64 = client_y - last_y;
last_pointer.set(Some((client_x, client_y)));
let yaw: f64 = angles.yaw.get() - dx * RAYTRACE_DRAG_SENSITIVITY;
let pitch: f64 = (angles.pitch.get() + dy * RAYTRACE_DRAG_SENSITIVITY).clamp(
-HALF_PI + RAYTRACE_PITCH_CLAMP,
HALF_PI - RAYTRACE_PITCH_CLAMP,
);
angles.yaw.set(yaw);
angles.pitch.set(pitch);
auto_rotate.set(false);
}))
}
pub(crate) fn raytrace_on_pointer_down(
last_pointer: Rc<Cell<Option<(f64, f64)>>>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
let client_x: f64 = client_x_from_event(&event);
let client_y: f64 = client_y_from_event(&event);
last_pointer.set(Some((client_x, client_y)));
}))
}
pub(crate) fn raytrace_on_pointer_up(
last_pointer: Rc<Cell<Option<(f64, f64)>>>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
last_pointer.set(None);
}))
}
fn first_touch_client(event: &Event) -> (f64, f64) {
let touches_value: JsValue = Reflect::get(
event.as_ref(),
&JsValue::from_str(RAYTRACE_EVENT_PROPERTY_TOUCHES),
)
.ok()
.unwrap_or(JsValue::NULL);
let touches: Array = touches_value.unchecked_into();
if touches.length() == 0 {
return (0.0, 0.0);
}
let touch: JsValue = touches.get(0);
let client_x: f64 = Reflect::get(&touch, &JsValue::from_str(RAYTRACE_EVENT_PROPERTY_CLIENT_X))
.ok()
.and_then(|value: JsValue| value.as_f64())
.unwrap_or_default();
let client_y: f64 = Reflect::get(&touch, &JsValue::from_str(RAYTRACE_EVENT_PROPERTY_CLIENT_Y))
.ok()
.and_then(|value: JsValue| value.as_f64())
.unwrap_or_default();
(client_x, client_y)
}
pub(crate) fn raytrace_on_touch_start(
last_pointer: Rc<Cell<Option<(f64, f64)>>>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
if event.cancelable() {
event.prevent_default();
}
let (client_x, client_y): (f64, f64) = first_touch_client(&event);
last_pointer.set(Some((client_x, client_y)));
}))
}
pub(crate) fn raytrace_on_touch_move(
angles: RayTraceCameraAngles,
auto_rotate: Signal<bool>,
last_pointer: Rc<Cell<Option<(f64, f64)>>>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
if event.cancelable() {
event.prevent_default();
}
let last: Option<(f64, f64)> = last_pointer.get();
let Some((last_x, last_y)) = last else {
return;
};
let (client_x, client_y): (f64, f64) = first_touch_client(&event);
let dx: f64 = client_x - last_x;
let dy: f64 = client_y - last_y;
last_pointer.set(Some((client_x, client_y)));
let yaw: f64 = angles.yaw.get() - dx * RAYTRACE_DRAG_SENSITIVITY;
let pitch: f64 = (angles.pitch.get() + dy * RAYTRACE_DRAG_SENSITIVITY).clamp(
-HALF_PI + RAYTRACE_PITCH_CLAMP,
HALF_PI - RAYTRACE_PITCH_CLAMP,
);
angles.yaw.set(yaw);
angles.pitch.set(pitch);
auto_rotate.set(false);
}))
}
pub(crate) fn raytrace_on_touch_end(
last_pointer: Rc<Cell<Option<(f64, f64)>>>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
if event.cancelable() {
event.prevent_default();
}
last_pointer.set(None);
}))
}
fn build_raytrace_sphere_pack_uniform(yaw: f64) -> Vec<f32> {
let (occluders, _): (Vec<Occluder>, Vector3D) = build_raytrace_scene(yaw);
let mut data: Vec<f32> = Vec::with_capacity(RAYTRACE_GPU_SPHERE_PACK_COUNT * 4);
for occluder in occluders.iter() {
let points: Vec<(Vector3D, f64)> = occluder.occluder_points();
if let Some(&(center, radius)) = points.first() {
data.push(center.get_x() as f32);
data.push(center.get_y() as f32);
data.push(center.get_z() as f32);
data.push(radius as f32);
}
}
while data.len() < RAYTRACE_GPU_SPHERE_PACK_COUNT * 4 {
data.push(0.0);
}
data
}
fn pack_raytrace_gpu_uniform(
yaw: f64,
pitch: f64,
width: f64,
height: f64,
sun_position: Vector3D,
) -> Vec<f32> {
let eye: Vector3D = compute_eye_position(yaw, pitch);
let (forward, right, up_true): (Vector3D, Vector3D, Vector3D) =
build_camera_basis(eye, yaw, pitch);
let sun_dir: Vector3D = raytrace_sun_direction(yaw);
let mut data: Vec<f32> = Vec::with_capacity(RAYTRACE_GPU_UNIFORM_VEC4_COUNT * 4);
let vectors: [Vector3D; 5] = [eye, forward, right, up_true, sun_dir];
for vector in vectors {
data.push(vector.get_x() as f32);
data.push(vector.get_y() as f32);
data.push(vector.get_z() as f32);
data.push(0.0);
}
data.extend_from_slice(&[1.0, 0.95, 0.85, 0.0]);
data.extend_from_slice(&[0.10, 0.10, 0.14, 0.0]);
data.extend_from_slice(&[width as f32, height as f32, 0.0, 0.0]);
data.extend_from_slice(&[
sun_position.get_x() as f32,
sun_position.get_y() as f32,
sun_position.get_z() as f32,
0.0,
]);
data.extend_from_slice(&[
sun_position.get_x() as f32,
GROUND_Y_TOP_FLOOR_LAMP as f32,
sun_position.get_z() as f32,
0.0,
]);
data
}
fn raytrace_set_loaded_delayed_canvas2d(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 raytrace_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 raytrace_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_raytrace_webgl_loop(state: UseRayTraceWebGl, angles: RayTraceCameraAngles) {
let init_state: UseRayTraceWebGl = state;
let loop_state: UseRayTraceWebGl = 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));
raytrace_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(
RAYTRACE_WEBGL_LOADING_CANVAS_SELECTOR,
RAYTRACE_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(
RAYTRACE_WEBGL_CANVAS_SELECTOR,
RAYTRACE_WIDTH,
RAYTRACE_HEIGHT,
);
let renderer: WebGlRenderer = match Engine::webgl_renderer(&config) {
Ok(value) => value,
Err(error) => {
Console::error(format!("[euv-engine][raytrace] 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(RAYTRACE_WEBGL_VERTEX_SHADER, RAYTRACE_WEBGL_FRAGMENT_SHADER)
{
Ok(value) => value,
Err(error) => {
Console::error(format!(
"[euv-engine][raytrace] 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]"));
let sphere_packs_location: Rc<Option<WebGlUniformLocation>> =
Rc::new(renderer.get_uniform_location(&program, "u_sphere_packs[0]"));
let clear_color: Rc<Cell<(f64, f64, f64)>> = Rc::new(Cell::new(
game_3d_canvas_clear_color(RAYTRACE_WEBGL_CANVAS_SELECTOR),
));
init_state.get_active().set(true);
raytrace_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(RAYTRACE_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(RAYTRACE_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 sphere_packs_location_for_loop: Rc<Option<WebGlUniformLocation>> =
sphere_packs_location.clone();
let clear_color_for_loop: Rc<Cell<(f64, f64, f64)>> = clear_color.clone();
let yaw_for_loop: Rc<Cell<f64>> = angles.yaw.clone();
let pitch_for_loop: Rc<Cell<f64>> = angles.pitch.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() || raytrace_canvas_detached(RAYTRACE_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
};
let anim_time: f64 = frame_time.min(0.25);
last_clone.set(current_time);
if loop_state.get_auto_rotate().get() {
yaw_for_loop.set(yaw_for_loop.get() + RAYTRACE_AUTO_YAW_SPEED * anim_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_raytrace_canvas_size(RAYTRACE_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 yaw: f64 = yaw_for_loop.get();
let pitch: f64 = pitch_for_loop.get();
let backing_w: f64 = f64::from(renderer.get_canvas().width());
let backing_h: f64 = f64::from(renderer.get_canvas().height());
let sun_position: Vector3D = raytrace_sun_position(yaw);
let uniform_data: Vec<f32> =
pack_raytrace_gpu_uniform(yaw, pitch, backing_w, backing_h, sun_position);
renderer.set_uniform_4fv(
&program_for_loop,
params_location_for_loop.as_ref().as_ref(),
&uniform_data,
);
let sphere_data: Vec<f32> = build_raytrace_sphere_pack_uniform(yaw);
renderer.set_uniform_4fv(
&program_for_loop,
sphere_packs_location_for_loop.as_ref().as_ref(),
&sphere_data,
);
let next_clear: (f64, f64, f64) =
game_3d_canvas_clear_color(RAYTRACE_WEBGL_CANVAS_SELECTOR);
if clear_color_for_loop.get() != next_clear {
clear_color_for_loop.set(next_clear);
}
let (r, g, b) = clear_color_for_loop.get();
renderer.render_frame(&program_for_loop, (r, g, b, 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_raytrace_webgpu_loop(state: UseRayTraceWebGpu, angles: RayTraceCameraAngles) {
let init_state: UseRayTraceWebGpu = state;
let loop_state: UseRayTraceWebGpu = 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));
raytrace_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(
RAYTRACE_WEBGPU_LOADING_CANVAS_SELECTOR,
RAYTRACE_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(
RAYTRACE_WEBGPU_CANVAS_SELECTOR,
RAYTRACE_WIDTH,
RAYTRACE_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][raytrace] 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(RAYTRACE_WEBGPU_SHADER);
let uniform_buffer: JsValue =
renderer.create_uniform_buffer(&[0.0; RAYTRACE_GPU_UNIFORM_VEC4_COUNT * 4]);
let sphere_pack_buffer: JsValue =
renderer.create_uniform_buffer(&[0.0; RAYTRACE_GPU_SPHERE_PACK_COUNT * 4]);
let bind_group: JsValue = renderer.create_bind_group(
&pipeline,
0,
&[
BindGroupEntry::Buffer {
binding: 0,
buffer: uniform_buffer.clone(),
offset: 0,
size: None,
},
BindGroupEntry::Buffer {
binding: 1,
buffer: sphere_pack_buffer.clone(),
offset: 0,
size: None,
},
],
);
let clear_color: Rc<Cell<(f64, f64, f64)>> = Rc::new(Cell::new(
game_3d_canvas_clear_color(RAYTRACE_WEBGPU_CANVAS_SELECTOR),
));
init_state.get_active().set(true);
raytrace_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 sphere_pack_buffer_rc: Rc<JsValue> = Rc::new(sphere_pack_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(RAYTRACE_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(RAYTRACE_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 sphere_pack_buffer_for_loop: Rc<JsValue> = sphere_pack_buffer_rc.clone();
let bind_group_for_loop: Rc<JsValue> = bind_group_rc.clone();
let clear_color_for_loop: Rc<Cell<(f64, f64, f64)>> = clear_color.clone();
let yaw_for_loop: Rc<Cell<f64>> = angles.yaw.clone();
let pitch_for_loop: Rc<Cell<f64>> = angles.pitch.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() || raytrace_canvas_detached(RAYTRACE_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
};
let anim_time: f64 = frame_time.min(0.25);
last_clone.set(current_time);
if loop_state.get_auto_rotate().get() {
yaw_for_loop.set(yaw_for_loop.get() + RAYTRACE_AUTO_YAW_SPEED * anim_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_raytrace_canvas_size(RAYTRACE_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 yaw: f64 = yaw_for_loop.get();
let pitch: f64 = pitch_for_loop.get();
let backing_w: f64 = f64::from(renderer.get_canvas().width());
let backing_h: f64 = f64::from(renderer.get_canvas().height());
let sun_position: Vector3D = raytrace_sun_position(yaw);
let uniform_data: Vec<f32> =
pack_raytrace_gpu_uniform(yaw, pitch, backing_w, backing_h, sun_position);
renderer.update_uniform_buffer(&buffer_for_loop, &uniform_data);
let sphere_data: Vec<f32> = build_raytrace_sphere_pack_uniform(yaw);
renderer.update_uniform_buffer(&sphere_pack_buffer_for_loop, &sphere_data);
let next_clear: (f64, f64, f64) =
game_3d_canvas_clear_color(RAYTRACE_WEBGPU_CANVAS_SELECTOR);
if clear_color_for_loop.get() != next_clear {
clear_color_for_loop.set(next_clear);
}
let (r, g, b) = clear_color_for_loop.get();
renderer.render_frame_with_bind_group(
&pipeline_for_loop,
&bind_group_for_loop,
(r, g, b, 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 raytrace_on_tab_select(
tab: Signal<RayTraceTab>,
value: RayTraceTab,
fullscreen: UseRayTraceFullscreen,
) -> 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_raytrace_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_raytrace_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_raytrace_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_raytrace_fullscreen_popstate(state: UseRayTraceFullscreen) -> usize {
Router::register_popstate_guard(Rc::new(move || {
if state.get_canvas_2d().get() {
exit_raytrace_fullscreen_from_popstate(state.get_canvas_2d());
true
} else if state.get_web_gl().get() {
exit_raytrace_fullscreen_from_popstate(state.get_web_gl());
true
} else if state.get_web_gpu().get() {
exit_raytrace_fullscreen_from_popstate(state.get_web_gpu());
true
} else {
false
}
}))
}
pub(crate) fn raytrace_on_enter_fullscreen(tab: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
enter_raytrace_fullscreen(tab);
}))
}
pub(crate) fn raytrace_on_exit_fullscreen(tab: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
exit_raytrace_fullscreen(tab);
Router::overlay_back(None);
}))
}