On the internet, nobody knows you're a badger.

Making a WebGL app with Speedy2D

Speedy2D lets you build cross-platform games and graphical applications using Rust. Version 1.3 has just been released with support for WebGL, in addition to the previously supported platforms Windows, Mac, and Linux.

This post will demonstrate how to create a WebGL app using Speedy2D. The app will work in any modern web browser, and with some minor configuration changes, you can build the same app for other platforms such as Windows.

To keep things simple, the app will just draw a blue circle at the current mouse position. Click here to see a live demo of the finished result (note: it’ll only work if your browser supports WebGL 2.0).

Note that you will need Rust installed to follow this tutorial – see the instructions here for how to do this.

Step 1: Create the Rust project

Use Cargo to create the project. You can use any name of your choice.

# Create the project
cargo new my-webgl-app

# Go to the project directory
cd my-webgl-app

Open Cargo.toml in a text editor and add Speedy2D to the dependencies section.

[dependencies]
speedy2d = "1.3.1"

For improved logging, we can also add the following:

log = "0.4"
wasm-logger = "0.2"
console_error_panic_hook = "0.1.6"

Now, run the first build, specifying the WebAssembly target:

# Set up Rust for WebAssembly builds
rustup target add wasm32-unknown-unknown

# Run a build with the WebAssembly target
cargo build --target wasm32-unknown-unknown

This will create a *.wasm file in the following location:

./target/wasm32-unknown-unknown/debug/my-webgl-app.wasm

We’ll use this file later – once we’ve actually written some code, we’ll need to rebuild it.

Step 2: Create an HTML page with a Canvas

Let’s create the actual web page that will host our app. Save the following HTML code to a file called index.html in your project directory. The filename my-webgl-app should be changed in both places to match your project name.

<!DOCTYPE html>
<html>

<head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
    <style>
        body {
            margin: 0px;
            padding: 0px;
        }
        canvas#my_canvas {
			position: absolute;
            width: 100%;
            height: 100%;
        }
    </style>
</head>

<body>

<canvas id="my_canvas"></canvas>

<script type="module" src="./my-webgl-app.js"></script>

<script type="module">
      import main from "./my-webgl-app.js";
      main();
</script>

</body>
</html>

This web page:

  • Contains a <canvas>, which is where Speedy2D will draw the graphics.
<canvas id="my_canvas"></canvas>
  • Loads our Javascript module – don’t forget to change my-webgl-app to the name of your project:
<script type="module" src="./my-webgl-app.js"></script>
  • Calls the main() function in our module:
<script type="module">
    import main from "./my-webgl-app.js";
    main();
</script>

It also has some CSS rules to set the size and layout of the canvas.

Step 3: Write the Rust code for our app

Writing a Speedy2D app for any platform looks something like this:

  • Create a struct implementing the WindowHandler trait.
  • Add implementations of the callbacks you’d like to receive, for example on_draw(), on_mouse_move(), or on_key_down().
  • In the main() function, create a Window or WebCanvas which uses your custom WindowHandler.

In this tutorial, let’s make a very simple app which just draws a blue circle at the current mouse position.

Overwrite the default contents of src/main.rs with the following Rust code:

use speedy2d::color::Color;
use speedy2d::dimen::Vector2;
use speedy2d::window::{WindowHandler, WindowHelper};
use speedy2d::{Graphics2D, WebCanvas};

struct MyHandler
{
	current_mouse_pos: Vector2<f32>
}

impl WindowHandler for MyHandler
{
	fn on_mouse_move(&mut self, h: &mut WindowHelper, pos: Vector2<f32>)
    {
		self.current_mouse_pos = pos;
		h.request_redraw();
    }

    fn on_draw(&mut self, _h: &mut WindowHelper, g: &mut Graphics2D)
    {
        g.clear_screen(Color::WHITE);
        g.draw_circle(self.current_mouse_pos, 50.0, Color::BLUE);
    }
}

fn main()
{
    wasm_logger::init(wasm_logger::Config::default());
    std::panic::set_hook(Box::new(console_error_panic_hook::hook));

    let handler = MyHandler { current_mouse_pos: Vector2::ZERO };

    WebCanvas::new_for_id("my_canvas", handler).unwrap();
}

Note that:

  • We define a new struct called MyHandler, which implements the on_mouse_move() and on_draw() callbacks.
  • When we get the on_mouse_move() callback, we save the position, and redraw the contents of the screen:
    fn on_mouse_move(&mut self, h: &mut WindowHelper, pos: Vector2<f32>)
    {
    	self.current_mouse_pos = pos;
    	h.request_redraw();
    }
  • In on_draw(), we clear the screen, and draw a circle at the saved mouse position.
  • In the main() function, we set up a logger and panic handler to help with debugging – any errors will be logged to the web console:
    wasm_logger::init(wasm_logger::Config::default());
    std::panic::set_hook(Box::new(console_error_panic_hook::hook));
  • We then create an instance of our custom handler, with the mouse position set to the default of (0, 0):
    let handler = MyHandler { current_mouse_pos: Vector2::ZERO };
  • And finally, attach our handler to the canvas in our web page:
    WebCanvas::new_for_id("my_canvas", handler).unwrap();

Step 4: Building

Time to build the project! First, let’s compile the Rust code into WebAssembly:

cargo build --target wasm32-unknown-unknown

Next, let’s create a directory called generated to store all the files needed for our web page:

mkdir generated

Copy the web page itself into that directory:

cp index.html generated/

Now we need to generate the Javascript bindings which will let us load the WebAssembly code from our web page.

Install the wasm-bindgen tool using cargo. If you’re using Linux, you may need to ensure pkg-config and OpenSSL are installed first (on Ubuntu: sudo apt install pkg-config libssl-dev ).

cargo install wasm-bindgen-cli

Then run the command below, replacing my-webgl-app with the name of your project. If in the future you do a release build, change debug to release.

wasm-bindgen target/wasm32-unknown-unknown/debug/my-webgl-app.wasm --out-dir generated --target web

Your project is now fully built, and the generated directory now contains the contents of your website.

Step 5: Testing

Due to the way WebAssembly code is loaded, your web page needs to be hosted on a webserver to test it.

Luckily, a crate called devserver makes it easy to run a testing server on our local machine.

First, install devserver:

cargo install devserver

Then, run it as follows:

devserver --path generated

Now, simply navigate to http://localhost:8080 to see your app in action!

(to use a different port, use the devserver --address option)

All done!

In a future tutorial I’ll show how to build this same app for Windows, Mac, and Linux, with just a couple of simple modifications. The custom WindowHandler we made will work without any modifications!

For another WebGL sample, see the following project on GitHub:

More information: