Skip to content

Commit

Permalink
move animation to its own file
Browse files Browse the repository at this point in the history
  • Loading branch information
kimonp committed Dec 26, 2023
1 parent 94c3398 commit fc291e2
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 99 deletions.
67 changes: 67 additions & 0 deletions src/animation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Animation frame control via a custom Dioxus hook.
use std::cell::RefCell;
use std::rc::Rc;

use dioxus::prelude::*;
use wasm_bindgen::prelude::Closure;

use crate::websys_utils::*;

/// A custom Dioxus hook that abstracts the request_animation_frame() and cancel_animation_frame() DOM calls.
///
/// Allows the caller to create a use_effect which watches the frame_id which can then
/// take an action each time a frame is advanced.
///
/// Returns two UseState variables: frame_running and frame_id.
/// * frame_running is true if frames are advancing.
/// * frame_id is incremented each time a new frame is run.
///
/// If frame_running is set to true, frames advance.
/// If frame_running is set to false, frames stop advancing.
pub fn use_animation_frame(cx: Scope, initial_state: bool) -> (&UseState<bool>, &UseState<i32>) {
let frame_running = use_state(cx, || initial_state);
let cancel_id = use_state(cx, || None::<i32>);
let frame_id = use_state(cx, || 0_i32);

use_effect(cx, (frame_running,), |(frame_running,)| {
to_owned![cancel_id, frame_id, frame_running];

// frame_loop_holder holds a closure that is passed to request_animation_frame().
// This closure is called each time an animation frame completes. We modify the universe
// inside this closure.
let frame_loop_holder = Rc::new(RefCell::new(None));
let frame_loop_holder_clone = frame_loop_holder.clone();

let cancel_id_clone = cancel_id.clone();
*frame_loop_holder.borrow_mut() = Some(Closure::<dyn FnMut()>::new(move || {
let new_id =
request_animation_frame(frame_loop_holder_clone.borrow().as_ref().unwrap());
cancel_id_clone.set(Some(new_id));

frame_id.with_mut(|id| {
*id = id.wrapping_add(1);
})
}));

async move {
// If we are requested to run, but we are not running, run
if *frame_running.get() && cancel_id.get().is_none() {
let new_id = request_animation_frame(frame_loop_holder.borrow().as_ref().unwrap());
cancel_id.set(Some(new_id));
}

// If we are requested to stop, but we are running, cancel
if !*frame_running.get() && cancel_id.get().is_some() {
cancel_id.with_mut(|maybe_id| {
if let Some(id) = maybe_id {
cancel_animation_frame(*id);
*maybe_id = None;
}
});
}
}
});

(frame_running, frame_id)
}
27 changes: 26 additions & 1 deletion src/frames_per_second.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
//! Calculates the frames per second and places the text in the given id.
use dioxus::prelude::*;

use crate::websys_utils::window;
use std::collections::VecDeque;

pub struct FramesPerSecond {
// Frames per second component that shows how quickly the app is rendering animation frames.
#[component]
pub fn FramesPerSecond(cx: Scope, frame_id: i32) -> Element {
let frames_per_second = use_ref(cx, FramesPerSecond::new);
let fps_text = use_state(cx, || frames_per_second.read().text());

// console_log!("Running app: {:?}", frame_id.get());

use_effect(cx, (frame_id,), |(_frame_id,)| {
to_owned![frames_per_second, fps_text];
async move {
frames_per_second.with_mut(|fps| {
fps.update_frame();
fps_text.modify(|_old_text| fps.text());
});
}
});

render! {
div { white_space: "pre", font_family: "monospace", fps_text.get().clone() }
}
}

struct FramesPerSecond {
last_timeframe_stamp: f64,
frames: VecDeque<f64>,
performance: web_sys::Performance,
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#[macro_use]
pub mod websys_utils;
pub mod animation;
pub mod frames_per_second;
pub mod universe;
133 changes: 35 additions & 98 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
use std::cell::RefCell;
use std::rc::Rc;

use dioxus::html::GlobalAttributes;
// use dioxus_elements::canvas;
// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types
use dioxus::prelude::*;

use game_of_life::animation::use_animation_frame;
use game_of_life::frames_per_second::FramesPerSecond;
use game_of_life::universe::{Cell, Universe, GRID_COLUMNS, GRID_ROWS};
use game_of_life::websys_utils::*;
use game_of_life::{console_log, frames_per_second};

use game_of_life::console_log;

use wasm_bindgen::prelude::Closure;
use wasm_bindgen::{JsCast, JsValue};

use web_sys::CanvasRenderingContext2d;

const CANVAS_ID: &str = "game-of-life-canvas";
const ANIMATION_ELEMENT_ID: &str = "animation-id-element";
const ANIMATION_ATTRIBUTE: &str = "animation-id";
const CELL_SIZE: u32 = 6; // px
const GRID_WIDTH: u32 = (CELL_SIZE + 1) * GRID_COLUMNS + 1;
const GRID_HEIGHT: u32 = (CELL_SIZE + 1) * GRID_ROWS + 1;
Expand All @@ -26,80 +24,13 @@ const GRID_COLOR: &str = "#CCCCCC";
const ALIVE_COLOR: &str = "#000000";
const DEAD_COLOR: &str = "#FFFFFF";

// extern crate console_error_panic_hook;
// use std::panic;

// #[wasm_bindgen]
// pub fn init_panic_hook() {
// // Better logging of panics in the browser
// console_error_panic_hook::set_once();
// }

// Entry point
fn main() {
// launch the dioxus app in a webview
// dioxus_desktop::launch(App);
dioxus_web::launch(App);
}

// A custom Dioxus hook that abstracts the request_animation_frame() and cancel_animation_frame() DOM calls.
//
// Allows the caller to create a use_effect which watches the frame_id which can then
// take an action each time a frame is advanced.
//
// Returns two UseState variables: frame_running and frame_id.
// * frame_running is true if frames are advancing.
// * frame_id is incremented each time a new frame is run.
//
// If frame_running is set to true, frames advance.
// If frame_running is set to false, frames stop advancing.
fn use_animation_frame(cx: Scope, initial_state: bool) -> (&UseState<bool>, &UseState<i32>) {
let frame_running = use_state(cx, || initial_state);
let cancel_id = use_state(cx, || None::<i32>);
let frame_id = use_state(cx, || 0_i32);

use_effect(cx, (frame_running,), |(frame_running,)| {
to_owned![cancel_id, frame_id, frame_running];

// frame_loop_holder holds a closure that is passed to request_animation_frame().
// This closure is called each time an animation frame completes. We modify the universe
// inside this closure.
let frame_loop_holder = Rc::new(RefCell::new(None));
let frame_loop_holder_clone = frame_loop_holder.clone();

let cancel_id_clone = cancel_id.clone();
*frame_loop_holder.borrow_mut() = Some(Closure::<dyn FnMut()>::new(move || {
let new_id =
request_animation_frame(frame_loop_holder_clone.borrow().as_ref().unwrap());
cancel_id_clone.set(Some(new_id));

frame_id.with_mut(|id| {
*id = id.wrapping_add(1);
})
}));

async move {
// If we are requested to run, but we are not running, run
if *frame_running.get() && cancel_id.get().is_none() {
let new_id = request_animation_frame(frame_loop_holder.borrow().as_ref().unwrap());
cancel_id.set(Some(new_id));
}

// If we are requested to stop, but we are running, cancel
if !*frame_running.get() && cancel_id.get().is_some() {
cancel_id.with_mut(|maybe_id| {
if let Some(id) = maybe_id {
cancel_animation_frame(*id);
*maybe_id = None;
}
});
}
}
});

(frame_running, frame_id)
}

#[component]
fn App(cx: Scope) -> Element {
let (frames_running, frame_id) = use_animation_frame(cx, false);
Expand Down Expand Up @@ -227,29 +158,6 @@ fn config_grid(universe: UseRef<Universe>) {
}


// Frames per second component that shows how quickly the app is rendering animation frames.
#[component]
fn FramesPerSecond(cx: Scope, frame_id: i32) -> Element {
let frames_per_second = use_ref(cx, FramesPerSecond::new);
let fps_text = use_state(cx, || frames_per_second.read().text());

// console_log!("Running app: {:?}", frame_id.get());

use_effect(cx, (frame_id,), |(_frame_id,)| {
to_owned![frames_per_second, fps_text];
async move {
frames_per_second.with_mut(|fps| {
fps.update_frame();
fps_text.modify(|_old_text| fps.text());
});
}
});

render! {
div { white_space: "pre", font_family: "monospace", fps_text.get().clone() }
}
}

// Draw the grid lines which contain the game of life cells.
fn draw_grid() {
let context = get_2d_context(CANVAS_ID);
Expand Down Expand Up @@ -285,7 +193,7 @@ fn get_grid_index(row: u32, col: u32) -> u32 {
}

// Draw all cells in the grid based on the state of the universe.
fn draw_cells(cells: &Vec<Cell>) {
fn draw_cells(cells: &[Cell]) {
let context = get_2d_context(CANVAS_ID);
// let cells = universe.cells();

Expand Down Expand Up @@ -321,4 +229,33 @@ fn fill_cells(context: &CanvasRenderingContext2d, cells: &[Cell], cell_type: Cel
}
}
}
}
}

// use web_sys::HtmlElement;

// #[component]
// fn Focus(cx: Scope) -> Element {
// let test = use_ref(cx, || None::<i32>);
// let input_element = use_ref(cx, || None::<HtmlElement>);

// // input { r#type: "text", r#ref: input_element }
// // input { r#type: "text" },
// // button { onclick: focus_input, "Focus Input" }
// render! {
// input { r#type: "text", ty: move |_| { input_element } }
// button {
// onclick: move |_| {
// input_element
// .with(|input_element| {
// input_element
// .clone()
// .map(|input_element| {
// let _ = input_element.focus();
// input_element
// });
// });
// },
// "Focus Input"
// }
// }
// }
50 changes: 50 additions & 0 deletions src/websys_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Short cuts for websys functions.
use wasm_bindgen::prelude::*;
use web_sys::CanvasRenderingContext2d;

#[macro_export]
macro_rules! console_log {
($($t:tt)*) => (
web_sys::console::log_1(&format!($($t)*).into())
)
}

pub fn window() -> web_sys::Window {
web_sys::window().expect("no global `window` exists")
}

/// Returns the id of the animation frame.
pub fn request_animation_frame(f: &Closure<dyn FnMut()>) -> i32 {
window()
.request_animation_frame(f.as_ref().unchecked_ref())
.expect("should register `requestAnimationFrame` OK")
}

/// Cancel a running aninmation frame.
pub fn cancel_animation_frame(animation_id: i32) {
window().cancel_animation_frame(animation_id).expect("Unable to cancel animation_frame")
}

/// Get the DOM document.
pub fn document() -> web_sys::Document {
window()
.document()
.expect("should have a document on window")
}

/// Return the 2d canvas context of the given element id.
pub fn get_2d_context(element_id: &str) -> CanvasRenderingContext2d {
let canvas_ele = document().get_element_by_id(element_id).expect("requested element not found");
let canvas_ele: web_sys::HtmlCanvasElement = canvas_ele
.dyn_into::<web_sys::HtmlCanvasElement>()
.map_err(|_| ())
.unwrap();

canvas_ele
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap()
}

0 comments on commit fc291e2

Please sign in to comment.