To write our game in Rust, we're going to need to draw to the screen, and for that, we'll use the HTML Canvas element using the 2D context. What the canvas provides is an API for drawing directly to the screen, without knowledge of WebGL or using an external tool. It's not the fastest technology in the world but it's perfectly suitable for our small game. Let's start converting our Rust app from "Hello World" to an application that draws a Sierpiński triangle.
Important Note
The Sierpiński triangle is a fractal image that is created by drawing a triangle, then subdividing that triangle into four triangles, and then subdividing those triangles into four triangles, and so on. It sounds complicated but, as with many fractals, is created from only a few lines of math:
- Add the canvas
Canvas is an HTML element that lets us draw to it freely, making it an ideal candidate for games. Indeed, at the time of writing, Adobe Flash is officially dead, and if you see a game on the internet, be it 2D or 3D, it's running in a canvas
element. Canvas can use WebGL or WebGPU for games, and WebAssembly will work quite well with those technologies, but they are out of the scope of this book. We'll be using the built-in Canvas 2D API and its 2D context. This means you won't have to learn a shading language, and we'll be able to get images on the screen very quickly. It also means that if you need to, you can find excellent documentation on the Mozilla Developer Network (MDN) Web Docs website: https://mzl.la/3tX5qPC.
To draw to the canvas, we'll need to add it to the web page. Open up static/index.html
and add underneath <body> tag <canvas id="canvas" tabindex="0" height="600" width="600">Your browser does not support the canvas.</canvas>
. The width and height are pretty arbitrary but seem appropriate for now. The "Your browser does not support the canvas.
" message will show up on browsers that don't support HTML Canvas, but there aren't many of those anymore.
Important Note
Make sure you don't delete the <script>
tag. That's running the JavaScript and WebAssembly you're building in this project!
- Clean up errors
Finally, we get to write some Rust code! Well, we get to delete some Rust code anyway. In the src/lib.rs
file, you'll see a function named main_js()
with the following code:
// This provides better error messages in debug mode.
// It's disabled in release mode so it doesn't bloat
up the file size.
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
You can go ahead and remove the comments and the [cfg(debug_annotations)]
annotation. For the time being, we'll leave that running in our build and will remove it when preparing for production with a feature flag.
Important Note
If you're seeing an error in your editor that says the console::log_1(&JsValue::from_str("Hello world!"))
code is missing an unsafe block, don't worry – that error is wrong. Unfortunately, it's a bug in rust-analyzer that's been addressed in this issue: https://bit.ly/3BbQ39m. You'll see this error with anything that uses procedural macros under the hood. If you're using an editor that supports experimental settings, you may be able to fix the problem; check the rust-analyzer.experimental.procAttrMacros
setting. When in doubt, check the output from npm run start
, as that is the more accurate source for compiler errors.
Tip
If you diverge from this book and decide to deploy, go to Chapter 10, Continuous Deployment, and learn how to hide that feature behind a feature flag in release mode, so you don't deploy code you don't need into production.
Removing that code will remove the warning: Found 'debug_assertions' in 'target.'cfg(...)'.dependencies'.
message on startup of the app. At this point, you may have noticed that I'm not telling you to restart the server after changes, and that's because npm start
runs the webpack-dev-server
, which automatically detects changes and then rebuilds and refreshes the app. Unless you're changing the webpack config, you shouldn't have to restart.
The current code
Up to now, I've been telling you what to do, and you've been blindly doing it because you're following along like a good reader. That's very diligent of you, if a little trusting, and it's time to take a look at the current source and see just what we have in our WebAssembly library. First, let's start with the use
directives.
use wasm_bindgen::prelude::*;
use web_sys::console;
The first import is the prelude
for wasm_bindgen
. This brings in the macros you'll see shortly, and a couple of types that are pretty necessary for writing Rust for the web. Fortunately, it's not a lot, and shouldn't pollute the namespace too much.
Important Note
"Pollute the namespace" refers to what can happen when you use the '*
' syntax and import everything from a given module. If the module has a lot of exported names, you have now those same names in your project, and they aren't obvious when you're coding. If, for instance, wasm_bindgen::prelude
had a function named add
in it and you also had a function named add
in your namespace, they would collide. You can work around this by using explicit namespaces when you call the functions, but then why use *
in the first place? By convention, many Rust packages have a module named prelude
, which can be imported via *
for ease of use; other modules should be imported with their full name.
The other import is web_sys::console
, which brings in the console
namespace from web_sys
, which in turn mimics the console
namespace in JavaScript. This is a good time to talk a little more in detail about what these two modules do. I've said it before but it probably bears repeating – wasm_bindgen
provides the capability to bind JavaScript functions so you can call them in WebAssembly and to expose your WebAssembly functions to JavaScript. There's that language again, the one we're trying to avoid by writing Rust, but it can't be avoided because we're working in a browser.
In fact, one of the limitations of WebAssembly is that it cannot manipulate the DOM, which is a fancy way of saying that it can't change the web page. What it can do is call functions in JavaScript, which in turn do that work. In addition, JavaScript knows nothing about your WebAssembly types, so any data that is passed to a JavaScript object is marshaled into shared memory and then pulled back out by JavaScript so that it can turn it into something it understands. This is a LOT of code to write over and over again, and that is what the wasm-bindgen
crate does for you. Later, we'll use it to bind our own custom bindings to third-party JavaScript code, but what about all the functions already built into the browser, such as console.log
? That's where web-sys
comes in. It uses wasm-bindgen
to bind to all the functions in the browser environment so that you don't have to manually specify them. Think of it as a helper crate that says, "Yeah, I know you'll need all these functions so I created them for you."
So, to sum up, wasm-bindgen
gives you the capability to communicate between WebAssembly and JavaScript, and web-sys
contains a large number of pre-created bindings. If you're particularly interested in how the calls between WebAssembly and JavaScript work, check out this article by Lin Clark, which explains it in great detail, and with pictures: https://hacks.mozilla.org/2018/10/calls-between-javascript-and-webassembly-are-finally-fast-%F0%9F%8E%89/.
The wee allocator
After the use statements you'll see a comment block referring to the `wee_alloc` feature, which is a WebAssembly allocator that uses much less memory than the default Rust allocator. We're not using it, and it was disabled in the Cargo.toml
file, so you can delete it from both the source code and Cargo.toml
.
The main
Finally, we get to the main part of our program:
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
The wasm_bindgen(start)
annotation exports main_js
so that it can be called by JavaScript, and the start
parameter identifies it as the starting point of the program. If you're curious, you can take a look at pkg/index_bg.wasm.d.ts
to see what was generated by it. You'll also want to take note of the return value, Result
, where the error type can be JsValue
, which represents an object owned by JavaScript and not Rust.
At this point, you may start to wonder how you'll keep track of what's JavaScript and what's Rust, and I'd advise you to not worry too much about it right now. There's a lot of jargon popping up and there's no way you'll keep it all in your head; just let it swim around in there and when it comes up again, I'll explain it again. JsValue
is just a representative JavaScript object in your Rust code.
Finally, let's look at the contents:
console_error_panic_hook::set_once();
// Your code goes here!
console::log_1(&JsValue::from_str("Hello world!"));
Ok(())
The first line sets the panic hook, which just means that any panics will be redirected to the web browser's console. You'll need it for debugging, and it's best to keep it at the beginning of the program. Our one line, our Hello World, is console::log_1(&JsValue::from_str("Hello world!"));
. That calls the JavaScript console.log
function, but it's using the version that's log_1
because the JavaScript version takes varying parameters. This is something that's going to come up again and again when using web-sys
, which is that JavaScript supports varargs
and Rust doesn't. So instead, many variations are created in the web-sys
module to match the alternatives. If a JavaScript function you expect doesn't exist, then take a look at the Rust documents for web-sys
(https://bit.ly/2NlRmOI) and see whether there are versions that are similar but built to account for multiple parameters.
Tip
A series of macros for several of the more commonly used functions (such as log
) could solve this problem, but that's an exercise for the reader.
Finally, the function returns Ok(())
, as is typical of Rust programs. Now that we've seen the generated code, let's break it down with our own.
Drawing a triangle
We've spent a lot of time digging into the code we currently have, and it's a lot to just write "Hello World" to the console. Why don't we have some fun and actually draw to the canvas?
What we're going to do is mimic the following JavaScript code in Rust:
canvas = window.document.getElementById("canvas")
context = canvas.getContext("2d")
context.moveTo(300, 0)
context.beginPath()
context.lineTo(0, 600)
context.lineTo(600, 600)
context.lineTo(300, 0)
context.closePath()
context.stroke()
context.fill()
This code grabs the canvas element we put in index.html
, grabs its 2D context, and then draws a black triangle. One way to draw a shape on the context is to draw a line path, then stroke, and, in this case, fill it. You can actually see this in the browser using the web developer tools built into most browsers. This screenshot is from Firefox:
Figure 1.2 – A simple canvas triangle
Let's do the same thing in our Rust program. You'll see that it's a little…different. Start with the quick addition of a use
statement at the top:
use wasm_bindgen::JsCast;
Then, replace the existing main_js
function with the following:
console_error_panic_hook::set_once();
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let canvas = document
.get_element_by_id("canvas")
.unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()
.unwrap();
let context = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap();
context.move_to(300.0, 0.0); // top of triangle
context.begin_path();
context.line_to(0.0, 600.0); // bottom left of triangle
context.line_to(600.0, 600.0); // bottom right of triangle
context.line_to(300.0, 0.0); // back to top of triangle
context.close_path();
context.stroke();
context.fill();
Ok(())
There are a few differences that stand out, but at a glance, you may just feel like Rust code is a lot noisier than JavaScript code, and that's true. You might be inclined to say that it's less elegant or isn't as clean, but I'd say that's in the eye of the beholder. JavaScript is a dynamically typed language and it shows. It ignores undefined
and null
, and can just crash if any of the values are not present. It uses duck typing to call all the functions on the context, which means that if the function is present, it simply calls it; otherwise, it throws exceptions.
Rust code takes a very different approach, one that favors explicitness and safety but at the cost of the code having extra noise. In Rust, you have to be more explicit when calling methods on structs, hence the casting, and you have to acknowledge null
or failed Result
types, hence all the unwraps. I've spent years using dynamic languages, including JavaScript, and I like them a lot. I certainly liked them a lot better than writing in C++, which I find overly verbose without really granting some of the safety advantages, but I think that with some tweaks, we can make Rust code nearly as elegant as JavaScript without glossing over exceptions and results.
My rant aside, if you're still running the program, you'll notice one minor detail – the Rust code doesn't compile! This leads me to the first thing we'll need to cover when translating JavaScript code to Rust code.
web-sys and feature flags
The web-sys
crate makes heavy use of feature flags to keep its size down. This means that every time you want to use a function and it doesn't exist, you'll need to check which feature flag it's tied to, which is in its documentation, and add it to Cargo.toml
. Fortunately, this is well documented and easy enough to do; we don't even need to restart the server!
Looking at our errors, we should see the following:
error[E0425]: cannot find function 'window' in crate 'web_sys'
--> src/lib.rs:18:27
|
18 | let window = web_sys::window().unwrap();
| ^^^^^^ not found in 'web_sys'
There are a few more errors of the same kind, but what you see here is that window
is not in the web_sys
module. Now, if you check the documentation for the window
function in web-sys
at https://bit.ly/3ak3sAR, you'll see that, yes, it does exist, but there is the This API requires the following crate features to be activated: Window
message.
Open the cargo.toml
file and look for dependencies.web-sys
. You'll see that it has a features
entry with just ["console"]
in it; go ahead and add "Window"
, "Document"
, "HtmlCanvasElement"
, "CanvasRenderingContext2d"
, and "Element"
to that list. To be clear, you don't need all those feature flags for just the window
function; that's all of the functions we're using.
You'll notice the project will rebuild automatically and should build successfully. If you look in the browser, you'll see your own black triangle! Let's extend it and learn a bit more about how we did it.
Tip
When a function you expect to exist on web-sys
doesn't, go and check the feature flags in the documents.
DOM interaction
You'll notice that the method for drawing the triangle after you get the context looks essentially the same as the method in JavaScript – draw a line path, stroke, and fill it. The code at the top that interacted with the DOM looks…different. Let's break down what's going on here:
Getting the Window
is just a function in the web-sys
crate, one you enabled when you added the Window
feature to Cargo.toml
. However, you'll notice it's got unwrap
at the end:
let window = web_sys::window().unwrap();
In JavaScript, window
can be null
or undefined
, at least theoretically, and in Rust, this gets translated into Option<Window>
. You can see that unwrap
is applied to the result of window()
, document()
, and get_element_by_id()
because all of them return Option<T>
.
What the heck is dyn_into
? Well, this oddity accounts for the difference between the way JavaScript does typing and the way Rust does. When we retrieve the canvas with get_element_by_id
, it returns Option<Element>
, and Element
does not have any functions relating to the canvas. In JavaScript, you can use dynamic typing to assume the element has the get_context
method, and if you're wrong, the program will throw an exception. This is anathema to Rust; indeed, this is a case where one developer's convenience is another developer's potential bug in hiding, so in order to use Element
, we have to call the dyn_into
function to cast it into HtmlCanvasElement
. This method was brought into scope with the `use wasm_bindgen::JsCast` declaration.
Important Note
Note that HtmlCanvasElement
, Document
, and Element
were all also feature flags you had to add in web-sys
.
After calling get_context("2d")
, we actually call unwrap
twice; that's not a typo. What's going on is that get_context
returns a Result<Option<Object>>
, so we unwrap it twice. This is another case where the game can't recover if this fails, so unwrap
is okay, but I wouldn't complain if you replaced those with expect
so that you can give a clearer error message.
A Sierpiński triangle
Now let's have some real fun, and draw a Sierpiński triangle a few levels deep. If you're up for a challenge, you can try and write the code yourself before following along with the solution presented here. The way the algorithm works is to draw the first triangle (the one you are already drawing) and then draw another three triangles, where the first triangle has the same top point but its other two points are at the halfway point on each side of the original triangle. Then, draw a second triangle on the lower left, with its top at the halfway point of the left side, its lower-right point at the halfway point of the bottom of the original triangle, and its lower-left point at the lower-left point of the original triangle. Finally, you create a third triangle in the lower-right corner of the original triangle. This leaves a "hole" in the middle shaped like an upside-down triangle. This is much easier to visualize than it is to explain, so how about a picture?
Figure 1.3 – A one-level Sierpiński triangle
Each of the numbered triangles was one that was drawn. The upside-down blue triangle is what's left of the original triangle because we didn't draw over it.
So that's one triangle subdivided into four. Now, the algorithm works recursively, taking each triangle and subdividing again. So, two levels deep, it looks like this:
Figure 1.4 – A two-level Sierpiński triangle
Note that it doesn't subdivide the upside-down triangle in the center, just the three purple ones that you created. Indeed, all the triangles with their points down are just "happy accidents" that make the shape look cool. You now know enough at this point to draw your own Sierpiński triangle, with one catch – you should remove the fill
statement on context. Otherwise, all the triangles will be filled black and you won't be able to see them. Go ahead and give it a try.
Drawing the Sierpiński triangle
So, did you give it a try? No, I wouldn't either; I guess we have a lot in common. To get started with creating a Sierpiński triangle, let's replace the hardcoded triangle with a triangle function. Here's the first pass at draw_triangle
:
fn draw_triangle(context: &web_sys::CanvasRenderingContext2d, points: [(f64, f64); 3]) {
let [top, left, right] = points;
context.move_to(top.0, top.1);
context.begin_path();
context.line_to(left.0, left.1);
context.line_to(right.0, right.1);
context.line_to(top.0, top.1);
context.close_path();
context.stroke();
}
There are a couple of small changes from the hard-coded version that we started with. The function takes a reference to the context and a list of three points. Points themselves are represented by tuples. We've also gotten rid of the fill
function, so we only have an empty triangle. Replace the inline draw_triangle
with the function call, which should look like this:
let context = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap();
draw_triangle(&context, [(300.0, 0.0), (0.0, 600.0), (600.0, 600.0)]);
Now that you're drawing one empty triangle, you're ready to start drawing the recursive triangles. Rather than starting with recursion, let's draw the first subdivision by drawing three more triangles. The first will have the same top point and two side points:
draw_triangle(&context, [(300.0, 0.0), (150.00, 300.0), (450.0, 300.0)]);
Note that the third tuple has an x halfway between 300.0
and 600.0
, not between 0
and 600.0
, because the top point of the triangle is halfway between the other two points. Also note that y gets larger as you go down, which is upside-down compared to many 3D systems. Now, let's add the lower-left and lower-right triangles:
draw_triangle(&context, [(150.0, 300.0), (0.0, 600.0), (300.0, 600.0)]);
draw_triangle(&context, [(450.0, 300.0), (300.0, 600.0), (600.0, 600.0)]);
Your triangles should look like this:
Figure 1.5 – Your triangles
You will start to see a pattern at this point, and we can begin to turn our hardcoded triangles into an algorithm. We'll create a function called sierpinski
that takes the context, the triangle dimensions, and a depth function so that we only draw as many triangles as we want, instead of drawing them to infinity and crashing the browser. Then, we'll move those functions we called into that function:
fn sierpinski(context: &web_sys::CanvasRenderingContext2d, points: [(f64, f64); 3], depth: u8) {
draw_triangle(&context, [(300.0, 0.0), (0.0, 600.0),
(600.0, 600.0)]);
draw_triangle(&context, [(300.0, 0.0), (150.00, 300.0),
(450.0, 300.0)]);
draw_triangle(&context, [(150.0, 300.0), (0.0, 600.0),
(300.0, 600.0)]);
draw_triangle(&context, [(450.0, 300.0), (300.0,
600.0), (600.0, 600.0)]);
}
This function currently ignores everything except the context, but you can replace those four draw_triangle
calls from main_js
and replace them with a call to sierpinski
:
sierpinski(&context, [(300.0, 0.0), (0.0, 600.0), (600.0, 600.0)], 2);
It's important that you only send a depth of 2
for now so that the image will continue to look the same as we progress. Think of this call as a proto-unit test, guaranteeing our behavior doesn't change while we refactor. Now, in sierpinski
, take the first triangle and have it use the passed-in points:
fn sierpinski(context: &web_sys::CanvasRenderingContext2d, points: [(f64, f64); 3], depth: u8) {
draw_triangle(&context, points);
...
Then, after drawing the triangle, reduce the depth by one and see if it is still greater than 0
. Then, draw the rest of the triangles:
...
let depth = depth - 1;
if depth > 0 {
draw_triangle(&context, [(300.0, 0.0), (150.00, 300.0),
(450.0, 300.0)]);
draw_triangle(&context, [(150.0, 300.0), (0.0, 600.0),
(300.0, 600.0)]);
draw_triangle(&context, [(450.0, 300.0), (300.0,
600.0), (600.0, 600.0)]);
}
Now, to complete the recursion, you can replace all those draw_triangle
calls with calls into sierpinski
:
if depth > 0 {
sierpinski(
&context,
[(300.0, 0.0), (150.00, 300.0), (450.0, 300.0)],
depth,
);
sierpinski(
&context,
[(150.0, 300.0), (0.0, 600.0), (300.0, 600.0)],
depth,
);
sierpinski(
&context,
[(450.0, 300.0), (300.0, 600.0), (600.0, 600.0)],
depth,
);
}
So far so good – you should still see a triangle subdivided into four triangles. Finally, we can actually calculate the midpoints of each line on the original triangle and use those to create the recursive triangles, instead of hardcoding them:
let [top, left, right] = points;
if depth > 0 {
let left_middle = ((top.0 + left.0) / 2.0, (top.1 +
left.1) / 2.0);
let right_middle = ((top.0 + right.0) / 2.0, (top.1 +
right.1) / 2.0);
let bottom_middle = (top.0, right.1);
sierpinski(&context, [top, left_middle, right_middle],
depth);
sierpinski(&context, [left_middle, left,
bottom_middle], depth);
sierpinski(&context, [right_middle, bottom_middle,
right], depth);
}
Calculating the midpoint of a line segment is done by taking the x and y coordinates of each end, adding those together, and then dividing them by two. While the preceding code works, let's make it clearer by writing a new function, as shown here:
fn midpoint(point_1: (f64, f64), point_2: (f64, f64)) -> (f64, f64) {
((point_1.0 + point_2.0) / 2.0, (point_1.1 + point_2.1)
/ 2.0)
}
Now, we can use that in the preceding function, for clarity:
if depth > 0 {
let left_middle = midpoint(top, left);
let right_middle = midpoint(top, right);
let bottom_middle = midpoint(left, right);
sierpinski(&context, [top, left_middle, right_middle],
depth);
sierpinski(&context, [left_middle, left,
bottom_middle], depth);
sierpinski(&context, [right_middle, bottom_middle,
right], depth);
}
If you've been following along, you should make sure you're still showing a triangle with four inside to ensure you haven't made any mistakes. Now for the big reveal – go ahead and change the depth to 5
in the original Sierpinski
call:
sierpinski(&context, [(300.0, 0.0), (0.0, 600.0), (600.0, 600.0)], 5);
You should see a recursive drawing of triangles, like this:
Figure 1.6 – A recursive drawing of triangles
Looking good! But what about those colors we saw in the original diagrams? They make it much more interesting.
When libraries aren't compatible
The earlier examples of this image had the triangles filled in with a different random color at each recursive layer. So, the first triangle was one color, three and four were another, the next nine another, and so on. It makes for a more interesting image and it provides a good example of what to do when a library isn't completely WebAssembly-compatible.
To create a random color, we'll need a random number generator, and that is not part of the standard library but instead found in a crate. You can add that crate by changing the Cargo.toml
file to include it as a dependency:
console_error_panic_hook = "0.1.7"
rand = "0.8.4"
When you do this, you'll get a compiler error that looks like the following (although your message may differ slightly):
error: target is not supported, for more information see: https://docs.rs/getrandom/#unsupported-targets
--> /usr/local/cargo/registry/src/github.com-
1ecc6299db9ec823/getrandom-0.2.2/src/lib.rs:213:9
|
213 | / compile_error!("target is not supported, for more information see: \
214 | | https://docs.rs/getrandom/#unsupported-targets");
This is a case where a transitive dependency, in this case getrandom
, does not compile on the WebAssembly target. In this case, it's an extremely helpful error message, and if you follow the link, you'll get the solution in the documentation. Specifically, you need to enable js
in the feature flags for getrandom
. Go back to your Cargo.toml
file and add the following:
getrandom = { version = "0.2.3", features = ["js"] }
This adds the getrandom
dependency with the js
feature enabled, and your code will begin compiling again. The lesson to take away from this is that not every Rust crate will compile on the WebAssembly target, and when that happens, you'll need to check the documents.
Tip
When a crate won't compile slowly, read the error message and follow the instructions. It's very easy to skim right over the reason the build is breaking, especially when you're frustrated.
Random colors
Now that we've got the random create building with our project, let's change the color of the triangles as we draw them to a random color. To do that, we'll set fillStyle
with a color before we draw the triangle, and we'll add a fill
command. This is, generally, how the Context2D
API works. You set up the state of the context and then execute commands with that state set. It takes a little getting used to but you'll get the hang of it. Let's add color
as a parameter of the three u8
tuples to draw_triangle
:
fn draw_triangle(
context: &web_sys::CanvasRenderingContext2d,
points: [(f64, f64); 3],
color: (u8, u8, u8),
) {
Important Note
Colors are represented here as three components, red, green, and blue, where each value can go from 0
to 255
. We're using tuples in this chapter because we can make progress quickly, but if it's starting to bother you, you're welcome to make proper struct
s.
Now that draw_triangle
needs a color, our application doesn't compile. Let's move to the sierpinski
function and pass a color to it as well. We're going to send the color to the sierpinski
function, instead of generating it there, so that we can get one color at every level. The first generation will be one solid color, then the second will all be one color, and then the third a third color, and so on. So let's add that:
fn sierpinski(
context: &web_sys::CanvasRenderingContext2d,
points: [(f64, f64); 3],
color: (u8, u8, u8),
depth: u8,
) {
draw_triangle(&context, points, color);
let depth = depth - 1;
let [top, left, right] = points;
if depth > 0 {
let left_middle = midpoint(top, left);
let right_middle = midpoint(top, right);
let bottom_middle = midpoint(left, right);
sierpinski(&context, [top, left_middle,
right_middle], color, depth);
sierpinski(&context, [left_middle, left,
bottom_middle], color, depth);
sierpinski(&context, [right_middle, bottom_middle,
right], color, depth);
}
}
I put color
as the third parameter and not the fourth because I think it looks better that way. Remember to pass color to the other calls. Finally, so that we can compile, we'll send a color to the initial sierpinski
call:
sierpinski(
&context,
[(300.0, 0.0), (0.0, 600.0), (600.0, 600.0)],
(0, 255, 0),
5,
);
Since this is an RGB color, (0, 255, 0)
represents green. Now, we've made our code compile, but it doesn't do anything, so let's work back downward from the original call and into the sierpinski
function again. Instead of just passing the color through, let's create a new tuple that has a random number for each component. You'll need to add use rand::prelude::*;
to the use declarations at the top. Then, add the following code to the sierpinski
function, after the if depth > 0
check:
let mut rng = thread_rng();
let next_color = (
rng.gen_range(0..255),
rng.gen_range(0..255),
rng.gen_range(0..255),
);
...
sierpinski(
&context,
top, left_middle, right_middle],
next_color,
depth,
);
sierpinski(
&context,
[left_middle, left, bottom_middle],
next_color,
depth,
);
sierpinski(
&context,
[right_middle, bottom_middle, right],
next_color,
depth,
);
Inside the depth check, we randomly generate next_color
and then pass it along to all the recursive sierpinski
calls. But of course, our output still doesn't look any different. We never changed draw_triangle
to change the color! This is going to be a little weird because the context.fillStyle
property takes DOMString
in JavaScript, so we'll need to do a conversion. At the top of draw_triangle
, add two lines:
let color_str = format!("rgb({}, {}, {})", color.0, color.1, color.2);
context.set_fill_style(&wasm_bindgen::JsValue::from_str(&color_str));
On line one, we convert our tuple of three unsigned integers to a string reading "rgb(255, 0, 255)"
, which is what the fillStyle
property expects. On the second line, we use set_fill_style
to set it, doing that funky conversion. There are two things that you need to understand with this function. The first is that, generally, JavaScript properties are just public and you set them, but web-sys
generates getter
and setter
functions. The second is that these generated functions frequently take JsValue
objects, which represent an object owned by JavaScript. Fortunately, wasm_bindgen
has factory functions for these, so we can create them easily and use the compiler as our guide.
Tip
Whenever you translate from JavaScript code to Rust, make sure that you check the documentation of the corresponding functions to see what types are needed. Passing a string to JavaScript isn't always as simple as you might think.
Finally, we actually need to fill the triangles to see those colors, so after context.stroke()
, you need to restore that context.fill()
method you deleted earlier, and ta-da!
Figure 1.7 – Filled triangles
You've done it, and you're ready to start creating a real game.