use super::*;
pub(crate) fn use_game_2d_state() -> UseGame2D {
UseGame2D {
running: App::use_signal(|| true),
fps: App::use_signal(|| 0.0),
ball_count: App::use_signal(|| 0),
total_spawned: App::use_signal(|| 0),
loaded: App::use_signal(|| false),
}
}
pub(crate) fn random_ball_color() -> &'static str {
let index: usize = (js_sys::Math::random() * GAME_2D_BALL_COLORS.len() as f64) as usize;
GAME_2D_BALL_COLORS[index % GAME_2D_BALL_COLORS.len()]
}
pub(crate) fn random_ball_radius() -> f64 {
let raw: f64 = js_sys::Math::random();
GAME_2D_BALL_MIN_RADIUS + raw * (GAME_2D_BALL_MAX_RADIUS - GAME_2D_BALL_MIN_RADIUS)
}
pub(crate) fn create_ball(position: Vector2D) -> Ball {
let angle: f64 = js_sys::Math::random() * PI - PI * 0.5;
let speed: f64 = GAME_2D_SPAWN_VELOCITY + js_sys::Math::random() * GAME_2D_SPAWN_VELOCITY;
Ball {
position,
velocity: Vector2D::new(angle.cos() * speed, -angle.sin() * speed.abs()),
radius: random_ball_radius(),
color: random_ball_color().to_string(),
}
}
pub(crate) fn game_2d_on_spawn_ball(
state: UseGame2D,
balls: Rc<RefCell<Vec<Ball>>>,
canvas_cache: CanvasCache,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
let current_count: usize = state.get_ball_count().get();
if current_count >= GAME_2D_MAX_BALLS {
return;
}
let (client_x, client_y): (f64, f64) = extract_mouse_client_position(&event);
let Some(canvas_element) = canvas_cache.0.borrow().as_ref().cloned() else {
return;
};
let rect: DomRect = canvas_element.get_bounding_client_rect();
let canvas_width: f64 = canvas_element.client_width() as f64;
let canvas_height: f64 = canvas_element.client_height() as f64;
let position: Vector2D =
map_client_to_canvas(client_x, client_y, &rect, canvas_width, canvas_height);
let ball: Ball = create_ball(position);
balls.borrow_mut().push(ball);
let new_count: usize = balls.borrow().len();
state.get_ball_count().set(new_count);
let total: usize = state.get_total_spawned().get();
state.get_total_spawned().set(total + 1);
}))
}
pub(crate) fn game_2d_on_toggle_pause(state: UseGame2D) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
let current: bool = state.get_running().get();
state.get_running().set(!current);
}))
}
pub(crate) fn game_2d_on_enter_fullscreen(
state: UseGame2DFullscreen,
tab: Signal<bool>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
enter_game_2d_fullscreen(state, tab);
}))
}
pub(crate) fn game_2d_on_exit_fullscreen(tab: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
exit_game_2d_fullscreen(tab);
Router::overlay_back(None);
}))
}
pub(crate) fn game_2d_on_clear(
state: UseGame2D,
balls: Rc<RefCell<Vec<Ball>>>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_: Event| {
balls.borrow_mut().clear();
state.get_ball_count().set(0);
}))
}
pub(crate) fn extract_mouse_client_position(event: &Event) -> (f64, f64) {
let mouse_event: &MouseEvent = event.unchecked_ref();
(
f64::from(mouse_event.client_x()),
f64::from(mouse_event.client_y()),
)
}
pub(crate) fn extract_touch_client_position(event: &Event) -> (f64, f64) {
let touch_event: &TouchEvent = event.unchecked_ref();
let touches: TouchList = touch_event.changed_touches();
if touches.length() == 0 {
return (0.0, 0.0);
}
let touch: Option<Touch> = touches.get(0);
let Some(touch) = touch else {
return (0.0, 0.0);
};
(f64::from(touch.client_x()), f64::from(touch.client_y()))
}
pub(crate) fn game_2d_on_touch_spawn_ball(
state: UseGame2D,
balls: Rc<RefCell<Vec<Ball>>>,
canvas_cache: CanvasCache,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
if event.cancelable() {
event.prevent_default();
}
let current_count: usize = state.get_ball_count().get();
if current_count >= GAME_2D_MAX_BALLS {
return;
}
let (client_x, client_y): (f64, f64) = extract_touch_client_position(&event);
let Some(canvas_element) = canvas_cache.0.borrow().as_ref().cloned() else {
return;
};
let rect: DomRect = canvas_element.get_bounding_client_rect();
let canvas_width: f64 = canvas_element.client_width() as f64;
let canvas_height: f64 = canvas_element.client_height() as f64;
let position: Vector2D =
map_client_to_canvas(client_x, client_y, &rect, canvas_width, canvas_height);
let ball: Ball = create_ball(position);
balls.borrow_mut().push(ball);
let new_count: usize = balls.borrow().len();
state.get_ball_count().set(new_count);
let total: usize = state.get_total_spawned().get();
state.get_total_spawned().set(total + 1);
}))
}
pub(crate) fn map_client_to_canvas(
client_x: f64,
client_y: f64,
canvas_rect: &DomRect,
canvas_width: f64,
canvas_height: f64,
) -> Vector2D {
let rect_width: f64 = canvas_rect.width();
let rect_height: f64 = canvas_rect.height();
if rect_width < EPSILON || rect_height < EPSILON {
return Vector2D::zero();
}
let scale_x: f64 = canvas_width / rect_width;
let scale_y: f64 = canvas_height / rect_height;
Vector2D::new(
(client_x - canvas_rect.left()) * scale_x,
(client_y - canvas_rect.top()) * scale_y,
)
}
pub(crate) fn handle_rescale_dirty(
resize_dirty_for_loop: &Rc<Cell<bool>>,
last_canvas_size_for_loop: &Rc<RefCell<(f64, f64)>>,
balls: &Rc<RefCell<Vec<Ball>>>,
prev_for_loop: &Rc<RefCell<Vec<Vector2D>>>,
canvas_cache: &CanvasCache,
_canvas_selector: &'static str,
) -> bool {
if !resize_dirty_for_loop.get() {
return false;
}
resize_dirty_for_loop.set(false);
let (new_w, new_h): (f64, f64) = canvas_cache
.0
.borrow()
.as_ref()
.map(|canvas| (canvas.client_width() as f64, canvas.client_height() as f64))
.unwrap_or((0.0, 0.0));
let (old_w, old_h) = *last_canvas_size_for_loop.borrow();
if old_w > 0.0 && old_h > 0.0 && new_w > 0.0 && new_h > 0.0 {
rescale_balls_to_canvas(
&mut balls.borrow_mut(),
&mut prev_for_loop.borrow_mut(),
old_w,
old_h,
new_w,
new_h,
);
}
if new_w > 0.0 && new_h > 0.0 {
*last_canvas_size_for_loop.borrow_mut() = (new_w, new_h);
}
true
}
pub(crate) fn handle_rescale_dirty_canvas2d(
resize_dirty_for_loop: &Rc<Cell<bool>>,
last_canvas_size_for_loop: &Rc<RefCell<(f64, f64)>>,
balls: &Rc<RefCell<Vec<Ball>>>,
prev_for_loop: &Rc<RefCell<Vec<Vector2D>>>,
canvas_cache: &CanvasCache,
context_clone: &Rc<RefCell<Option<SsaaCanvas>>>,
) {
let (css_w, css_h): (f64, f64) =
read_canvas_size(GAME_2D_CANVAS_SELECTOR).unwrap_or((0.0, 0.0));
let (cached_w, cached_h) = *last_canvas_size_for_loop.borrow();
let (canvas_w, canvas_h): (f64, f64) = canvas_cache
.0
.borrow()
.as_ref()
.map(|canvas| (canvas.client_width() as f64, canvas.client_height() as f64))
.unwrap_or((0.0, 0.0));
let (effective_w, effective_h): (f64, f64) = if canvas_w > 0.0 && canvas_h > 0.0 {
(canvas_w, canvas_h)
} else {
(css_w, css_h)
};
let css_mismatch: bool = effective_w > 0.0
&& effective_h > 0.0
&& (cached_w <= 0.0
|| cached_h <= 0.0
|| (effective_w - cached_w).abs() > 1.5
|| (effective_h - cached_h).abs() > 1.5);
if resize_dirty_for_loop.get() {
resize_dirty_for_loop.set(false);
if cached_w > 0.0 && cached_h > 0.0 && effective_w > 0.0 && effective_h > 0.0 {
rescale_balls_to_canvas(
&mut balls.borrow_mut(),
&mut prev_for_loop.borrow_mut(),
cached_w,
cached_h,
effective_w,
effective_h,
);
}
if effective_w > 0.0 && effective_h > 0.0 {
*last_canvas_size_for_loop.borrow_mut() = (effective_w, effective_h);
*context_clone.borrow_mut() = None;
}
} else if css_mismatch && effective_w > 0.0 && effective_h > 0.0 {
rescale_balls_to_canvas(
&mut balls.borrow_mut(),
&mut prev_for_loop.borrow_mut(),
cached_w,
cached_h,
effective_w,
effective_h,
);
*last_canvas_size_for_loop.borrow_mut() = (effective_w, effective_h);
*context_clone.borrow_mut() = None;
}
}
pub(crate) fn update_balls(
balls: &mut [Ball],
delta_time: f64,
canvas_width: f64,
canvas_height: f64,
) {
let sub_dt: f64 = delta_time / GAME_2D_PHYSICS_SUBSTEPS as f64;
let gravity: Vector2D = Vector2D::new(0.0, GAME_2D_GRAVITY);
for _ in 0..GAME_2D_PHYSICS_SUBSTEPS {
let damping: f64 = (1.0 - GAME_2D_LINEAR_DAMPING * sub_dt).max(0.0);
for ball in balls.iter_mut() {
ball.velocity += gravity.scaled(sub_dt);
ball.velocity = ball.velocity.scaled(damping);
ball.position += ball.velocity.scaled(sub_dt);
}
for ball in balls.iter_mut() {
resolve_wall_collision(ball, canvas_width, canvas_height);
}
for _ in 0..GAME_2D_COLLISION_ITERATIONS {
let count: usize = balls.len();
for i in 0..count {
for j in (i + 1)..count {
let (left, right) = balls.split_at_mut(j);
resolve_ball_collision(&mut left[i], &mut right[0]);
}
}
}
resolve_stuck_balls(balls);
}
}
pub(crate) fn resolve_wall_collision(ball: &mut Ball, canvas_width: f64, canvas_height: f64) {
if ball.position.get_x() - ball.radius < 0.0 {
ball.position.set_x(ball.radius);
let velocity_x: f64 = ball.velocity.get_x();
ball.velocity.set_x(velocity_x.abs() * GAME_2D_RESTITUTION);
}
if ball.position.get_x() + ball.radius > canvas_width {
ball.position.set_x(canvas_width - ball.radius);
let velocity_x: f64 = ball.velocity.get_x();
ball.velocity.set_x(-velocity_x.abs() * GAME_2D_RESTITUTION);
}
if ball.position.get_y() - ball.radius < 0.0 {
ball.position.set_y(ball.radius);
let velocity_y: f64 = ball.velocity.get_y();
ball.velocity.set_y(velocity_y.abs() * GAME_2D_RESTITUTION);
}
if ball.position.get_y() + ball.radius > canvas_height {
ball.position.set_y(canvas_height - ball.radius);
let velocity_y: f64 = ball.velocity.get_y();
ball.velocity.set_y(-velocity_y.abs() * GAME_2D_RESTITUTION);
}
}
pub(crate) fn rescale_balls_to_canvas(
balls: &mut Vec<Ball>,
prev_positions: &mut Vec<Vector2D>,
old_width: f64,
old_height: f64,
new_width: f64,
new_height: f64,
) {
if old_width <= 0.0 || old_height <= 0.0 || new_width <= 0.0 || new_height <= 0.0 {
return;
}
let scale_x: f64 = new_width / old_width;
let scale_y: f64 = new_height / old_height;
for ball in balls.iter_mut() {
ball.position.set_x(ball.position.get_x() * scale_x);
ball.position.set_y(ball.position.get_y() * scale_y);
let max_x: f64 = (new_width - ball.radius).max(ball.radius);
let max_y: f64 = (new_height - ball.radius).max(ball.radius);
if ball.position.get_x() < ball.radius {
ball.position.set_x(ball.radius);
} else if ball.position.get_x() > max_x {
ball.position.set_x(max_x);
}
if ball.position.get_y() < ball.radius {
ball.position.set_y(ball.radius);
} else if ball.position.get_y() > max_y {
ball.position.set_y(max_y);
}
}
for prev in prev_positions.iter_mut() {
prev.set_x(prev.get_x() * scale_x);
prev.set_y(prev.get_y() * scale_y);
}
if new_width * new_height < old_width * old_height {
let canvas_area: f64 = new_width * new_height;
if canvas_area > 0.0 {
let mut total_ball_area: f64 = balls
.iter()
.map(|b| b.radius * b.radius * std::f64::consts::PI)
.sum();
let cap: f64 = canvas_area * GAME_2D_MAX_BALL_AREA_RATIO;
let mut trim_count: usize = 0;
while total_ball_area > cap && trim_count < balls.len() {
let removed: &Ball = &balls[trim_count];
total_ball_area -= removed.radius * removed.radius * std::f64::consts::PI;
trim_count += 1;
}
if trim_count > 0 {
balls.drain(..trim_count);
if prev_positions.len() > balls.len() {
prev_positions.truncate(balls.len());
} else if prev_positions.len() < balls.len() {
while prev_positions.len() < balls.len() {
let idx: usize = prev_positions.len();
prev_positions.push(balls[idx].position);
}
}
}
}
}
}
pub(crate) fn resolve_ball_collision(a: &mut Ball, b: &mut Ball) {
let delta: Vector2D = b.position - a.position;
let distance: f64 = delta.magnitude();
let radius_sum: f64 = a.radius + b.radius;
if distance >= radius_sum {
return;
}
let normal: Vector2D = if distance < EPSILON {
Vector2D::right()
} else {
delta.scaled(1.0 / distance)
};
let overlap: f64 = radius_sum - distance;
let mass_a: f64 = a.radius * a.radius;
let mass_b: f64 = b.radius * b.radius;
let total_mass: f64 = mass_a + mass_b;
a.position -= normal.scaled(overlap * (mass_b / total_mass));
b.position += normal.scaled(overlap * (mass_a / total_mass));
let relative_velocity: Vector2D = b.velocity - a.velocity;
let velocity_along_normal: f64 = relative_velocity.dot(normal);
if velocity_along_normal > 0.0 {
return;
}
let impulse_magnitude: f64 =
-(1.0 + GAME_2D_RESTITUTION) * velocity_along_normal / (1.0 / mass_a + 1.0 / mass_b);
let impulse: Vector2D = normal.scaled(impulse_magnitude);
a.velocity -= impulse.scaled(1.0 / mass_a);
b.velocity += impulse.scaled(1.0 / mass_b);
}
pub(crate) fn resolve_stuck_balls(balls: &mut [Ball]) {
let count: usize = balls.len();
if count == 0 {
return;
}
let stuck_threshold: usize = count / 4;
let mut stuck_count: Vec<u32> = vec![0; count];
for i in 0..count {
let (left, right) = balls.split_at(i + 1);
let ball_i: &Ball = &left[i];
for (offset, ball_j) in right.iter().enumerate() {
let j: usize = i + 1 + offset;
let dx: f64 = ball_j.position.get_x() - ball_i.position.get_x();
let dy: f64 = ball_j.position.get_y() - ball_i.position.get_y();
let distance_sq: f64 = dx * dx + dy * dy;
let radius_sum: f64 = ball_i.radius + ball_j.radius;
if distance_sq < (radius_sum - GAME_2D_STUCK_MIN_OVERLAP).max(0.0).powi(2) {
stuck_count[i] = stuck_count[i].saturating_add(1);
stuck_count[j] = stuck_count[j].saturating_add(1);
}
}
}
for (ball, &hits) in balls.iter_mut().zip(stuck_count.iter()) {
if hits as usize > stuck_threshold {
ball.radius *= GAME_2D_STUCK_RADIUS_SHRINK;
}
}
}
pub(crate) fn snapshot_ball_positions(prev_positions: &mut Vec<Vector2D>, balls: &[Ball]) {
prev_positions.truncate(balls.len());
for (index, ball) in balls.iter().enumerate() {
if index < prev_positions.len() {
prev_positions[index] = ball.position;
} else {
prev_positions.push(ball.position);
}
}
}
pub(crate) fn interpolate_balls(
balls: &[Ball],
prev_positions: &[Vector2D],
alpha: f64,
) -> Vec<Ball> {
balls
.iter()
.enumerate()
.map(|(index, ball): (usize, &Ball)| {
let mut render_ball: Ball = ball.clone();
if let Some(prev_position) = prev_positions.get(index) {
render_ball.position = prev_position.lerp(ball.position, alpha);
}
render_ball
})
.collect()
}
pub(crate) fn render_balls_with_ssaa(
ssaa_canvas: &SsaaCanvas,
balls: &[Ball],
canvas_width: f64,
canvas_height: f64,
) {
let context: &CanvasRenderingContext2d = ssaa_canvas.get_offscreen_context();
context.clear_rect(0.0, 0.0, canvas_width, canvas_height);
let fill_style_key: JsValue = JsValue::from_str(GAME_2D_PROPERTY_FILL_STYLE);
for ball in balls {
let _ = Reflect::set(context, &fill_style_key, &JsValue::from_str(&ball.color));
context.begin_path();
let _ = context.arc(
ball.position.get_x(),
ball.position.get_y(),
ball.radius,
0.0,
std::f64::consts::TAU,
);
context.fill();
}
ssaa_canvas.present();
}
pub(crate) fn read_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()))
}
pub(crate) fn acquire_game_2d_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_canvas_size(GAME_2D_CANVAS_SELECTOR)?;
let ssaa_canvas: SsaaCanvas = SsaaCanvas::from_selector_with_scale(
GAME_2D_CANVAS_SELECTOR,
canvas_width,
canvas_height,
scale_factor,
)?;
let document_value: Document = window_value.document()?;
let element: Element = document_value
.query_selector(GAME_2D_CANVAS_SELECTOR)
.ok()
.flatten()?;
let display_canvas: HtmlCanvasElement = element.unchecked_into();
Some((display_canvas, ssaa_canvas))
}
pub(crate) fn draw_game_2d_loading(target_selector: &str, color_source_selector: &str) {
let Some(window_value): Option<Window> = window() else {
return;
};
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 Some((canvas_width, canvas_height)) = read_canvas_size(target_selector) else {
return;
};
let Some(ssaa_canvas) = SsaaCanvas::from_selector_with_scale(
target_selector,
canvas_width,
canvas_height,
scale_factor,
) else {
return;
};
let context: &CanvasRenderingContext2d = ssaa_canvas.get_offscreen_context();
context.clear_rect(0.0, 0.0, canvas_width, canvas_height);
let fill_style_key: JsValue = JsValue::from_str(GAME_2D_PROPERTY_FILL_STYLE);
let Some(document_value): Option<Document> = window_value.document() else {
return;
};
let computed_style: Option<CssStyleDeclaration> = document_value
.query_selector(color_source_selector)
.ok()
.flatten()
.and_then(|element: Element| window_value.get_computed_style(&element).ok().flatten());
let background_color: String = computed_style
.as_ref()
.and_then(|style: &CssStyleDeclaration| {
style
.get_property_value(GAME_2D_PROPERTY_BACKGROUND_COLOR)
.ok()
})
.unwrap_or_default();
if !background_color.is_empty() {
let _ = Reflect::set(
context,
&fill_style_key,
&JsValue::from_str(&background_color),
);
context.fill_rect(0.0, 0.0, canvas_width, canvas_height);
}
let font_size: f64 = canvas_height * GAME_2D_LOADING_FONT_SIZE_RATIO;
let font: String = format!("{font_size}px {GAME_2D_LOADING_FONT_FAMILY}");
let loading_color: String = computed_style
.and_then(|style: CssStyleDeclaration| {
style.get_property_value(GAME_2D_LOADING_COLOR_VAR).ok()
})
.filter(|color: &String| !color.is_empty())
.unwrap_or_else(|| "#ffffff".to_string());
let _ = Reflect::set(context, &fill_style_key, &JsValue::from_str(&loading_color));
context.set_font(&font);
context.set_text_align("center");
context.set_text_baseline("middle");
let _ = context.fill_text(
GAME_2D_LOADING_TEXT,
canvas_width * 0.5,
canvas_height * 0.5,
);
ssaa_canvas.present();
}
fn 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);
}
pub(crate) fn start_game_2d_loop(
state: UseGame2D,
balls: Rc<RefCell<Vec<Ball>>>,
canvas_cache: CanvasCache,
) {
let canvas_ssaa: Rc<RefCell<Option<SsaaCanvas>>> = Rc::new(RefCell::new(None));
let resize_dirty: Rc<Cell<bool>> = Rc::new(Cell::new(false));
let accumulator: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
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 raf_id: Rc<Cell<Option<i32>>> = Rc::new(Cell::new(None));
let closure_cell: RafClosureCell = Rc::new(MaybeEngineCell::new());
let prev_positions: Rc<RefCell<Vec<Vector2D>>> = Rc::new(RefCell::new(Vec::new()));
let last_canvas_size: Rc<RefCell<(f64, f64)>> = Rc::new(RefCell::new((0.0, 0.0)));
let acc_clone: Rc<Cell<f64>> = accumulator.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 raf_clone: Rc<Cell<Option<i32>>> = raf_id.clone();
let cell_clone: RafClosureCell = closure_cell.clone();
let context_clone: Rc<RefCell<Option<SsaaCanvas>>> = canvas_ssaa.clone();
let dirty_clone: Rc<Cell<bool>> = resize_dirty.clone();
let prev_clone: Rc<RefCell<Vec<Vector2D>>> = prev_positions.clone();
let last_canvas_size_clone: Rc<RefCell<(f64, f64)>> = last_canvas_size.clone();
let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
if game_2d_canvas_detached(GAME_2D_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 {
GAME_2D_FIXED_TIMESTEP
} else {
(current_time - prev).min(0.25)
};
last_clone.set(current_time);
handle_rescale_dirty_canvas2d(
&dirty_clone,
&last_canvas_size_clone,
&balls,
&prev_clone,
&canvas_cache,
&context_clone,
);
if state.get_running().get() {
acc_clone.set(acc_clone.get() + frame_time);
while acc_clone.get() >= GAME_2D_FIXED_TIMESTEP {
snapshot_ball_positions(&mut prev_clone.borrow_mut(), &balls.borrow());
let (cached_w, cached_h) = *last_canvas_size_clone.borrow();
let (cw, ch): (f64, f64) = if cached_w > 0.0 && cached_h > 0.0 {
(cached_w, cached_h)
} else {
canvas_cache
.0
.borrow()
.as_ref()
.map(|canvas| (canvas.client_width() as f64, canvas.client_height() as f64))
.unwrap_or((0.0, 0.0))
};
update_balls(&mut balls.borrow_mut(), GAME_2D_FIXED_TIMESTEP, cw, ch);
acc_clone.set(acc_clone.get() - GAME_2D_FIXED_TIMESTEP);
}
}
let alpha: f64 = (acc_clone.get() / GAME_2D_FIXED_TIMESTEP).clamp(0.0, 1.0);
if context_clone.borrow().is_none()
&& let Some((canvas_el, ssaa_canvas)) = acquire_game_2d_ssaa_canvas()
{
*canvas_cache.0.borrow_mut() = Some(canvas_el);
*context_clone.borrow_mut() = Some(ssaa_canvas);
}
if let Some(ssaa_canvas) = context_clone.borrow().as_ref() {
let (canvas_width, canvas_height): (f64, f64) = canvas_cache
.0
.borrow()
.as_ref()
.map(|canvas| (canvas.client_width() as f64, canvas.client_height() as f64))
.unwrap_or((0.0, 0.0));
let render_balls: Vec<Ball> =
interpolate_balls(&balls.borrow(), &prev_clone.borrow(), alpha);
render_balls_with_ssaa(ssaa_canvas, &render_balls, canvas_width, canvas_height);
}
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 state_for_start: UseGame2D = state;
let start_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
state_for_start.get_loaded().set(true);
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,
GAME_2D_LOOP_START_DELAY_MILLIS,
)
.unwrap_or_default();
start_timeout_clone.set(Some(timeout_id));
let loading_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
draw_game_2d_loading(GAME_2D_CANVAS_SELECTOR, GAME_2D_CANVAS_SELECTOR);
}));
let loading_callback: Function = loading_closure.as_ref().unchecked_ref::<Function>().clone();
loading_closure.forget();
let _ =
start_window.set_timeout_with_callback_and_timeout_and_arguments_0(&loading_callback, 0);
let debounce_timer: Rc<Cell<Option<i32>>> = Rc::new(Cell::new(None));
let dirty_for_event: Rc<Cell<bool>> = resize_dirty.clone();
let timer_for_event: Rc<Cell<Option<i32>>> = debounce_timer.clone();
let debounce_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
dirty_for_event.set(true);
}));
let debounce_callback: Function = debounce_closure
.as_ref()
.unchecked_ref::<Function>()
.clone();
debounce_closure.forget();
let Some(timeout_window): Option<Window> = window() else {
return;
};
App::use_window_event("resize", move || {
let old_timer: Option<i32> = timer_for_event.get();
if let Some(timer_id) = old_timer {
timeout_window.clear_timeout_with_handle(timer_id);
}
let new_timer: i32 = timeout_window
.set_timeout_with_callback_and_timeout_and_arguments_0(
&debounce_callback,
GAME_2D_RESIZE_DEBOUNCE_MILLIS,
)
.unwrap_or_default();
timer_for_event.set(Some(new_timer));
});
App::use_cleanup(move || {
if let Some(cancel_id) = raf_id.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);
}
if let Some(timer_id) = debounce_timer.get() {
let Some(window_value): Option<Window> = window() else {
return;
};
window_value.clear_timeout_with_handle(timer_id);
}
let _: Option<_> = closure_cell.try_take();
});
}
pub(crate) fn use_game_2d_webgpu_state() -> UseGame2DWebGpu {
UseGame2DWebGpu {
fps: App::use_signal(|| 0.0),
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_game_2d_webgl_state() -> UseGame2DWebGl {
UseGame2DWebGl {
fps: App::use_signal(|| 0.0),
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 game_2d_canvas_element(canvas_selector: &str) -> Option<HtmlCanvasElement> {
let window_value: Window = window()?;
let document_value: Document = window_value.document()?;
let element: Element = document_value
.query_selector(canvas_selector)
.ok()
.flatten()?;
Some(element.unchecked_into())
}
pub(crate) fn game_2d_canvas_detached(canvas_selector: &str) -> bool {
game_2d_canvas_element(canvas_selector).is_none()
}
pub(crate) fn game_2d_hex_to_rgb(color: &str) -> (f32, f32, f32) {
let hex: &str = color.strip_prefix('#').unwrap_or(color);
let channel = |range: Range<usize>| -> f32 {
hex.get(range)
.and_then(|part: &str| u8::from_str_radix(part, 16).ok())
.map(|value: u8| f32::from(value) / 255.0)
.unwrap_or(1.0)
};
(channel(0..2), channel(2..4), channel(4..6))
}
pub(crate) fn game_2d_canvas_clear_color(canvas_selector: &str) -> (f64, f64, f64) {
let Some(window_value): Option<Window> = window() else {
return (0.0, 0.0, 0.0);
};
let background: String = window_value
.document()
.and_then(|document: Document| document.query_selector(canvas_selector).ok().flatten())
.and_then(|element: Element| window_value.get_computed_style(&element).ok().flatten())
.and_then(|style: CssStyleDeclaration| style.get_property_value("background-color").ok())
.unwrap_or_default();
let Some(inner) = background
.split('(')
.nth(1)
.and_then(|value: &str| value.strip_suffix(')'))
else {
return (0.0, 0.0, 0.0);
};
let mut channels = inner
.split(',')
.filter_map(|part: &str| part.trim().parse::<f64>().ok());
let r: f64 = channels.next().unwrap_or_default() / 255.0;
let g: f64 = channels.next().unwrap_or_default() / 255.0;
let b: f64 = channels.next().unwrap_or_default() / 255.0;
(r, g, b)
}
fn game_2d_ball_gpu_record(ball: &Ball, _dpr: f64) -> ([f32; 4], [f32; 4]) {
let (r, g, b) = game_2d_hex_to_rgb(&ball.color);
(
[
ball.position.get_x() as f32,
ball.position.get_y() as f32,
ball.radius as f32,
0.0,
],
[r, g, b, 1.0],
)
}
fn pack_game_2d_balls_webgpu(
balls: &[Ball],
canvas_width: f64,
canvas_height: f64,
dpr: f64,
) -> Vec<f32> {
let mut data: Vec<f32> = vec![canvas_width as f32, canvas_height as f32, 0.0, 0.0];
for ball in balls {
let (pos_radius, color) = game_2d_ball_gpu_record(ball, dpr);
data.extend_from_slice(&pos_radius);
data.extend_from_slice(&color);
}
data.resize(4 + GAME_2D_MAX_BALLS * 8, 0.0);
data
}
fn pack_game_2d_balls_webgl(balls: &[Ball], dpr: f64) -> (Vec<f32>, Vec<f32>) {
let mut pos_radius: Vec<f32> = Vec::with_capacity(balls.len() * 4);
let mut colors: Vec<f32> = Vec::with_capacity(balls.len() * 4);
for ball in balls {
let (ball_pos_radius, ball_color) = game_2d_ball_gpu_record(ball, dpr);
pos_radius.extend_from_slice(&ball_pos_radius);
colors.extend_from_slice(&ball_color);
}
(pos_radius, colors)
}
pub(crate) fn game_2d_on_tab_select(
tab: Signal<Game2DTab>,
value: Game2DTab,
fullscreen: UseGame2DFullscreen,
) -> 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 start_game_2d_webgpu_loop(
state: UseGame2DWebGpu,
game: UseGame2D,
balls: Rc<RefCell<Vec<Ball>>>,
canvas_cache: CanvasCache,
) {
let init_state: UseGame2DWebGpu = state;
let loop_state: UseGame2DWebGpu = 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 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_2D_RESIZE_DEBOUNCE_MILLIS,
)
.unwrap_or_default();
resize_timer_for_event.set(Some(new_timer));
});
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();
App::use_cleanup(move || {
cancelled_for_cleanup.set(true);
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(timer_id) = resize_timer_for_cleanup.get() {
let Some(window_value): Option<Window> = window() else {
return;
};
window_value.clear_timeout_with_handle(timer_id);
}
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_2d_loading(
GAME_2D_WEBGPU_LOADING_CANVAS_SELECTOR,
GAME_2D_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(
GAME_2D_WEBGPU_CANVAS_SELECTOR,
GAME_2D_CANVAS_WIDTH,
GAME_2D_CANVAS_HEIGHT,
);
let renderer: Result<WebGpuRenderer, WebGpuInitError> =
Engine::webgpu_renderer(&config).await;
if cancelled_for_init.get() {
return;
}
let renderer: WebGpuRenderer = match renderer {
Ok(value) => value,
Err(error) => {
Console::error(format!("[euv-engine][game_2d] 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(GAME_2D_WEBGPU_SHADER);
let uniform_buffer: JsValue =
renderer.create_uniform_buffer(&vec![0.0; 4 + GAME_2D_MAX_BALLS * 8]);
let bind_group: JsValue = renderer.create_uniform_bind_group(&pipeline, &uniform_buffer);
*canvas_cache.0.borrow_mut() = game_2d_canvas_element(GAME_2D_WEBGPU_CANVAS_SELECTOR);
let clear_color: Rc<Cell<(f64, f64, f64)>> = Rc::new(Cell::new(
game_2d_canvas_clear_color(GAME_2D_WEBGPU_CANVAS_SELECTOR),
));
let accumulator: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
init_state.get_active().set(true);
set_loaded_delayed(init_state.get_loaded(), GAME_2D_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 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 clear_color_for_loop: Rc<Cell<(f64, f64, f64)>> = clear_color.clone();
let acc_clone: Rc<Cell<f64>> = accumulator.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 prev_positions: Rc<RefCell<Vec<Vector2D>>> = Rc::new(RefCell::new(Vec::new()));
let prev_for_loop: Rc<RefCell<Vec<Vector2D>>> = prev_positions.clone();
let last_canvas_size: Rc<RefCell<(f64, f64)>> = Rc::new(RefCell::new((0.0, 0.0)));
let last_canvas_size_for_loop: Rc<RefCell<(f64, f64)>> = last_canvas_size.clone();
let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
if cancelled_for_loop.get() || game_2d_canvas_detached(GAME_2D_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 {
GAME_2D_FIXED_TIMESTEP
} else {
(current_time - prev).min(0.25)
};
last_clone.set(current_time);
let resize_dirty: bool = handle_rescale_dirty(
&resize_dirty_for_loop,
&last_canvas_size_for_loop,
&balls,
&prev_for_loop,
&canvas_cache,
GAME_2D_WEBGPU_CANVAS_SELECTOR,
);
if game.get_running().get() {
acc_clone.set(acc_clone.get() + frame_time);
while acc_clone.get() >= GAME_2D_FIXED_TIMESTEP {
snapshot_ball_positions(&mut prev_for_loop.borrow_mut(), &balls.borrow());
let (cached_w, cached_h) = *last_canvas_size_for_loop.borrow();
let (cw, ch): (f64, f64) = if cached_w > 0.0 && cached_h > 0.0 {
(cached_w, cached_h)
} else {
canvas_cache
.0
.borrow()
.as_ref()
.map(|canvas| {
(canvas.client_width() as f64, canvas.client_height() as f64)
})
.unwrap_or((0.0, 0.0))
};
update_balls(&mut balls.borrow_mut(), GAME_2D_FIXED_TIMESTEP, cw, ch);
acc_clone.set(acc_clone.get() - GAME_2D_FIXED_TIMESTEP);
}
}
let alpha: f64 = (acc_clone.get() / GAME_2D_FIXED_TIMESTEP).clamp(0.0, 1.0);
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 (cached_w, cached_h) = *last_canvas_size_for_loop.borrow();
let (canvas_width, canvas_height): (f64, f64) = if cached_w > 0.0 && cached_h > 0.0 {
(cached_w, cached_h)
} else {
canvas_cache
.0
.borrow()
.as_ref()
.map(|canvas| (canvas.client_width() as f64, canvas.client_height() as f64))
.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 {
let _ = renderer.resize(new_physical_width, new_physical_height);
}
let render_balls: Vec<Ball> =
interpolate_balls(&balls.borrow(), &prev_for_loop.borrow(), alpha);
let uniform_data: Vec<f32> =
pack_game_2d_balls_webgpu(&render_balls, canvas_width, canvas_height, dpr);
let vertex_count: u32 = (render_balls.len() * 6) as u32;
renderer.update_uniform_buffer(&buffer_for_loop, &uniform_data);
let next_clear: (f64, f64, f64) =
game_2d_canvas_clear_color(GAME_2D_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),
vertex_count,
);
}
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 use_game_2d_fullscreen_state() -> UseGame2DFullscreen {
UseGame2DFullscreen {
canvas_2d: App::use_signal(|| false),
web_gl: App::use_signal(|| false),
web_gpu: App::use_signal(|| false),
}
}
pub(crate) fn enter_game_2d_fullscreen(state: UseGame2DFullscreen, tab: Signal<bool>) {
tab.set(true);
let _ = state;
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_game_2d_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_game_2d_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_game_2d_fullscreen_popstate(state: UseGame2DFullscreen) -> usize {
Router::register_popstate_guard(Rc::new(move || {
if state.get_canvas_2d().get() {
exit_game_2d_fullscreen_from_popstate(state.get_canvas_2d());
true
} else if state.get_web_gl().get() {
exit_game_2d_fullscreen_from_popstate(state.get_web_gl());
true
} else if state.get_web_gpu().get() {
exit_game_2d_fullscreen_from_popstate(state.get_web_gpu());
true
} else {
false
}
}))
}
pub(crate) fn start_game_2d_webgl_loop(
state: UseGame2DWebGl,
game: UseGame2D,
balls: Rc<RefCell<Vec<Ball>>>,
canvas_cache: CanvasCache,
) {
let init_state: UseGame2DWebGl = state;
let loop_state: UseGame2DWebGl = 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 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_2D_RESIZE_DEBOUNCE_MILLIS,
)
.unwrap_or_default();
resize_timer_for_event.set(Some(new_timer));
});
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();
App::use_cleanup(move || {
cancelled_for_cleanup.set(true);
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(timer_id) = resize_timer_for_cleanup.get() {
let Some(window_value): Option<Window> = window() else {
return;
};
window_value.clear_timeout_with_handle(timer_id);
}
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_2d_loading(
GAME_2D_WEBGL_LOADING_CANVAS_SELECTOR,
GAME_2D_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(
GAME_2D_WEBGL_CANVAS_SELECTOR,
GAME_2D_CANVAS_WIDTH,
GAME_2D_CANVAS_HEIGHT,
);
let renderer: WebGlRenderer = match Engine::webgl_renderer(&config) {
Ok(value) => value,
Err(error) => {
Console::error(format!("[euv-engine][game_2d] 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(GAME_2D_WEBGL_VERTEX_SHADER, GAME_2D_WEBGL_FRAGMENT_SHADER)
{
Ok(value) => value,
Err(error) => {
Console::error(format!(
"[euv-engine][game_2d] webgl program failed: {error}"
));
init_state.get_init_error_code().set("WEBGL_PROGRAM_ERROR");
init_state.get_loaded().set(true);
return;
}
};
let canvas_size_location: Rc<Option<WebGlUniformLocation>> =
Rc::new(renderer.get_uniform_location(&program, "u_canvas_size"));
let ball_pos_radius_location: Rc<Option<WebGlUniformLocation>> =
Rc::new(renderer.get_uniform_location(&program, "u_ball_pos_radius[0]"));
let ball_color_location: Rc<Option<WebGlUniformLocation>> =
Rc::new(renderer.get_uniform_location(&program, "u_ball_color[0]"));
*canvas_cache.0.borrow_mut() = game_2d_canvas_element(GAME_2D_WEBGL_CANVAS_SELECTOR);
let clear_color: Rc<Cell<(f64, f64, f64)>> = Rc::new(Cell::new(
game_2d_canvas_clear_color(GAME_2D_WEBGL_CANVAS_SELECTOR),
));
let accumulator: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
init_state.get_active().set(true);
set_loaded_delayed(init_state.get_loaded(), GAME_2D_LOADING_MIN_MILLIS);
*renderer_rc.borrow_mut() = Some(renderer);
let program_rc: Rc<WebGlProgram> = Rc::new(program);
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 canvas_size_location_for_loop: Rc<Option<WebGlUniformLocation>> =
canvas_size_location.clone();
let ball_pos_radius_location_for_loop: Rc<Option<WebGlUniformLocation>> =
ball_pos_radius_location.clone();
let ball_color_location_for_loop: Rc<Option<WebGlUniformLocation>> =
ball_color_location.clone();
let clear_color_for_loop: Rc<Cell<(f64, f64, f64)>> = clear_color.clone();
let acc_clone: Rc<Cell<f64>> = accumulator.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 prev_positions: Rc<RefCell<Vec<Vector2D>>> = Rc::new(RefCell::new(Vec::new()));
let prev_for_loop: Rc<RefCell<Vec<Vector2D>>> = prev_positions.clone();
let last_canvas_size: Rc<RefCell<(f64, f64)>> = Rc::new(RefCell::new((0.0, 0.0)));
let last_canvas_size_for_loop: Rc<RefCell<(f64, f64)>> = last_canvas_size.clone();
let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
if cancelled_for_loop.get() || game_2d_canvas_detached(GAME_2D_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 {
GAME_2D_FIXED_TIMESTEP
} else {
(current_time - prev).min(0.25)
};
last_clone.set(current_time);
let resize_dirty: bool = handle_rescale_dirty(
&resize_dirty_for_loop,
&last_canvas_size_for_loop,
&balls,
&prev_for_loop,
&canvas_cache,
GAME_2D_WEBGL_CANVAS_SELECTOR,
);
if game.get_running().get() {
acc_clone.set(acc_clone.get() + frame_time);
while acc_clone.get() >= GAME_2D_FIXED_TIMESTEP {
snapshot_ball_positions(&mut prev_for_loop.borrow_mut(), &balls.borrow());
let (cached_w, cached_h) = *last_canvas_size_for_loop.borrow();
let (cw, ch): (f64, f64) = if cached_w > 0.0 && cached_h > 0.0 {
(cached_w, cached_h)
} else {
canvas_cache
.0
.borrow()
.as_ref()
.map(|canvas| {
(canvas.client_width() as f64, canvas.client_height() as f64)
})
.unwrap_or((0.0, 0.0))
};
update_balls(&mut balls.borrow_mut(), GAME_2D_FIXED_TIMESTEP, cw, ch);
acc_clone.set(acc_clone.get() - GAME_2D_FIXED_TIMESTEP);
}
}
let alpha: f64 = (acc_clone.get() / GAME_2D_FIXED_TIMESTEP).clamp(0.0, 1.0);
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 (cached_w, cached_h) = *last_canvas_size_for_loop.borrow();
let (canvas_width, canvas_height): (f64, f64) = if cached_w > 0.0 && cached_h > 0.0 {
(cached_w, cached_h)
} else {
canvas_cache
.0
.borrow()
.as_ref()
.map(|canvas| (canvas.client_width() as f64, canvas.client_height() as f64))
.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 {
renderer.resize(new_physical_width, new_physical_height);
}
let render_balls: Vec<Ball> =
interpolate_balls(&balls.borrow(), &prev_for_loop.borrow(), alpha);
let (pos_radius_data, color_data) = pack_game_2d_balls_webgl(&render_balls, dpr);
let vertex_count: i32 = (render_balls.len() * 6) as i32;
renderer.set_uniform_2f(
&program_for_loop,
canvas_size_location_for_loop.as_ref().as_ref(),
canvas_width as f32,
canvas_height as f32,
);
renderer.set_uniform_4fv(
&program_for_loop,
ball_pos_radius_location_for_loop.as_ref().as_ref(),
&pos_radius_data,
);
renderer.set_uniform_4fv(
&program_for_loop,
ball_color_location_for_loop.as_ref().as_ref(),
&color_data,
);
let next_clear: (f64, f64, f64) =
game_2d_canvas_clear_color(GAME_2D_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), vertex_count);
}
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));
});
}