Using Canvas in a dioxus web App #999
Replies: 6 comments
-
That looks close. To run some code after an element is mounted, you need to put the code in an effect: #![allow(non_snake_case)]
use crate::universe::{Cell, Universe};
use dioxus::prelude::*;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
mod universe;
static CELL_SIZE: u32 = 5;
static GRID_COLOR: &str = "#CCCCCC";
static DEAD_COLOR: &str = "#FFFFFF";
static ALIVE_COLOR: &str = "#000000";
fn main() {
dioxus_web::launch(App);
}
fn App(cx: Scope) -> Element {
let universe = Universe::new();
let canvas_height = (CELL_SIZE + 1) * universe.height() + 1;
let canvas_width = (CELL_SIZE + 1) * universe.width() + 1;
use_effect!(cx, move |()| async move {
let context = start_render_loop();
loop {
render_loop(context, universe);
// Some async delay with https://docs.rs/gloo-timers/0.2.6/gloo_timers/, or https://docs.rs/async-std/latest/async_std/task/fn.sleep.html
async_std::task::sleep(Duration::from_millis(10)).await;
}
]);
cx.render(rsx! {
canvas { id: "game-of-life-canvas", height: canvas_height as i64, width: canvas_width as i64}
})
}
fn start_render_loop(universe: Universe) -> web_sys::CanvasRenderingContext2d {
let window = web_sys::window().expect("global window does not exists");
let document = window.document().expect("expecting a document on window");
let canvas = document
.get_element_by_id("game-of-life-canvas")
.expect("expecting a canvas in the document")
.dyn_into::<web_sys::HtmlCanvasElement>()
.unwrap();
let context = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap();
context
}
fn render_loop(context: web_sys::CanvasRenderingContext2d, mut universe: Universe) {
universe.tick();
draw_cells(&context, &universe);
draw_grid(&context, &universe);
let closure = Closure::once(move || render_loop(context, universe));
web_sys::window()
.expect("global window does not exists")
.request_animation_frame(closure.as_ref().unchecked_ref())
.unwrap();
}
fn draw_grid(context: &web_sys::CanvasRenderingContext2d, universe: &Universe) {
context.begin_path();
context.set_stroke_style(&GRID_COLOR.into());
for i in 0..=universe.width() {
context.move_to((i * (CELL_SIZE + 1) + 1) as f64, 0f64);
context.line_to(
(i * (CELL_SIZE + 1) + 1) as f64,
((CELL_SIZE + 1) * universe.height() + 1) as f64,
);
}
for i in 0..=universe.height() {
context.move_to(0f64, (i * (CELL_SIZE + 1) + 1) as f64);
context.line_to(
((CELL_SIZE + 1) * universe.width() + 1) as f64,
(i * (CELL_SIZE + 1) + 1) as f64,
);
}
context.stroke();
}
fn draw_cells(context: &web_sys::CanvasRenderingContext2d, universe: &Universe) {
context.begin_path();
draw_cells_with_style(context, universe, |cell| cell == Cell::Alive, ALIVE_COLOR);
draw_cells_with_style(context, universe, |cell| cell == Cell::Dead, DEAD_COLOR);
context.stroke();
}
fn draw_cells_with_style(
context: &web_sys::CanvasRenderingContext2d,
universe: &Universe,
condition: impl Fn(Cell) -> bool,
style: &str,
) {
context.set_fill_style(&style.into());
for row in 0..universe.height() {
for col in 0..universe.width() {
let idx = universe[(row, col)];
if !condition(universe.cells()[idx as usize]) {
continue;
}
context.fill_rect(
(col * (CELL_SIZE + 1) + 1) as f64,
(row * (CELL_SIZE + 1) + 1) as f64,
CELL_SIZE as f64,
CELL_SIZE as f64,
);
}
}
} This code will be simplified a bit with #894: You can remove the element id and move the future into the event listener |
Beta Was this translation helpful? Give feedback.
-
Thanks a lot, after some tweaking the example worked. |
Beta Was this translation helpful? Give feedback.
-
Switching from requst_animation_frame to a sleep was to yield back to the render. Because Dioxus runs on a single thread if you run a blocking infinite loop it will never render anything else. If you have something async (like a sleep), it will yield back to Dioxus' scheduler allowing it to render other parts of your application. If this is your whole application you may not need it, but if you have another area of your application it is necessary. If request_animation_frame doesn't block the rust code until the next animation frame this may not be necessary. Try this example to see what blocking code does to the rendering process: use dioxus::prelude::*;
fn main() {
dioxus_web::launch(app);
}
fn app(cx: Scope) -> Element {
let count = use_ref(cx, || 0);
use_effect(cx, (), |_| {
to_owned![count];
async move {
loop {
// Try commenting out this line to see what happens when running blocking tasks
async_std::task::sleep(std::time::Duration::from_millis(10)).await;
*count.write() += 1;
}
}
});
cx.render(rsx! {
h1 { "High-Five counter: {count.read()}" }
button { onclick: move |_| *count.write() += 1, "Up high!" }
button { onclick: move |_| *count.write() -= 1, "Down low!" }
})
} |
Beta Was this translation helpful? Give feedback.
-
Thanks so much for helping and explaining. |
Beta Was this translation helpful? Give feedback.
-
Once again we have some difficulties, we would like to add a play/pause button. let game_of_life = Rc::new(RefCell::new(None::<GameOfLife>));
let game_of_life_init = game_of_life.clone();
use_effect(cx, (), move |()| async move {
let context = get_context_2d();
*game_of_life_init.borrow_mut() = Some(GameOfLife::new(context, universe));
});
use_effect(cx, (is_playing,), move |(is_playing,)| async move {
while *is_playing.get() {
if let Some(game_of_life) = game_of_life.borrow_mut().as_mut() {
game_of_life.tick();
}
async_std::task::sleep(Duration::from_millis(1)).await;
}
}); The code so far: |
Beta Was this translation helpful? Give feedback.
-
(I'm transferring this to a discussion because this isn't an feature request or bug with Dioxus. Discussions are a good place to ask questions. We also have a discord server with a more active help form) Effects do not stop after they are rerun (documented here). Because is_playing is a use_state value, the get and deref value are from the last execution of the component. If you want to get the current value you can use the current function. (See also #619) When you set is_playing to false, the old future should stop running which should fix your issue. let game_of_life = Rc::new(RefCell::new(None::<GameOfLife>));
let game_of_life_init = game_of_life.clone();
use_effect(cx, (), move |()| async move {
let context = get_context_2d();
*game_of_life_init.borrow_mut() = Some(GameOfLife::new(context, universe));
});
use_effect(cx, (is_playing,), move |(is_playing,)| async move {
// Read the current value instead of the value from the last component re-render
while *is_playing.current() {
if let Some(game_of_life) = game_of_life.borrow_mut().as_mut() {
game_of_life.tick();
}
async_std::task::sleep(Duration::from_millis(1)).await;
}
}); |
Beta Was this translation helpful? Give feedback.
-
Hi, we are trying to implement game-of-life in Dioxus.
We want to show how convert the following implementation from js to dioxus.
https://github.com/almondtools/etka-life
We already have the canvas renderd, but we can't figure out where to start the render loop, because it needs the canvas to already be in the document.
We are open to completly change the implementation to be idiomatic dioxus, although we would like to get it running with the canvas to not go to far from the original example.
Thanks a lot!
Beta Was this translation helpful? Give feedback.
All reactions