New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support running Turtle logic in the browser via WebAssembly #53

Closed
wants to merge 46 commits into
base: master
from
Commits
Jump to file or symbol
Failed to load files and symbols.
+1,548 −874
Diff settings

Always

Just for now

Copy path View file
@@ -25,3 +25,6 @@ _site/
.jekyll-metadata
# End of https://www.gitignore.io/api/jekyll
.idea
*.iml
Copy path View file
@@ -0,0 +1,30 @@
Turtle for the web!
# Setup
- `rustup update`
- `rustup target add wasm32-unknown-unknown --toolchain nightly`
# Building
To build wasm files for all examples, run:
```
find . -name '*.wasm' -delete && cargo +nightly build \
--no-default-features --features=canvas \
--target=wasm32-unknown-unknown --release \
--examples
```
Cargo doesn't seem very smart about updating the `.wasm` file, so we always delete it.
Once the `.wasm` file is built, run a basic web server (like `python -m SimpleHTTPServer 9000`) and open [http://localhost:9000/canvas.html](http://localhost:9000/canvas.html).
# Architecture
On a desktop OS, Turtle uses a separate process to handle rendering and turtle state. This allows the turtle control logic to take over the main thread, which in turn allows a convenient programming model: the user provides `main()`, and as long as Turtle initialization is done right, everything proceeds fine. Rendering commands, etc, are done via stdin/stdout between the control process and render process.
On the web, we don't have the built-in start point of `main()`, so control flow will have to be different. Rendering can still be done with Piston's abstraction, but we need to render to a `<canvas>`, so we need a `Graphics` implementation that writes to an RGBA pixel buffer.
The Rust logic runs in a Worker and periodically sends copies of the pixel buffer to the main thread to be displayed, which is inefficient but does work. However, there isn't a synchronous way to communicate between the Worker and the main thread, so even if we buffered up input events in the main thread, there wouldn't be a way to get them to the Rust logic since that logic never yields control of the worker thread. There are asynchronous ways to hand off data, but that would require the Rust logic halting and resuming later, which the current structure does not allow.
Copy path View file
@@ -30,11 +30,24 @@ serde = "^1.0.23"
serde_derive = "^1.0.23"
serde_json = "^1.0.6"
piston_window = "^0.73.0"
piston_window = { version = "^0.73.0", optional = true }
# make sure to update low level piston dependencies when updating piston_window
piston2d-graphics = "^0.23.0"
interpolation = "^0.1.0"
rand = "^0.3.18"
pistoncore-input = "^0.20.0"
# current master has wasm32 support; released version does not
rand = { git = "https://github.com/rust-lang-nursery/rand.git", rev = "0cb85a2b9f3e0a24f1634faeb68230ec5b919340" }
[features]
default = ["desktop"]
# Render via a window on a desktop GUI OS.
desktop = ["piston_window"]
# Render via wasm to canvas. Mutually exclusive with "desktop" feature.
canvas = []
# The reason we do this is because doctests don't get cfg(test)
# See: https://github.com/rust-lang/cargo/issues/4669
#
Copy path View file
@@ -0,0 +1,43 @@
<html>
<head>
<title>turtle via canvas</title>
<meta charset="utf-8"/>
<script type="text/javascript">
window.onload = (_) => {
const exampleForm = document.getElementById("example");
const exampleInput = document.getElementById("example-name");
exampleForm.onsubmit = (_) => {
const worker = new Worker('worker.js');
const canvas = document.getElementById('screen');
const ctx = canvas.getContext('2d');
worker.onmessage = (e) => {
if (e.data['type'] === 'updateCanvas') {
const img = new ImageData(e.data['pixels'], canvas.width, canvas.height);
ctx.putImageData(img, 0, 0);
}
};
worker.postMessage({
'type': 'start',
'exampleName': exampleInput.value,
'width': canvas.width,
'height': canvas.height
});
return false;
};
}
</script>
</head>
<body>
<form id="example">
<input type="text" placeholder="example name" value="randomcolors" id="example-name"/>
<input type="submit"/>
</form>
<canvas id="screen" width="800px" height="600px" style="border: black 1px solid"></canvas>
</body>
</html>
Copy path View file
@@ -1,11 +1,10 @@
#[macro_use]
extern crate turtle;
use turtle::Turtle;
use turtle::event::Key::{Left, Right};
use turtle::Event::KeyPressed;
fn main() {
let mut turtle = Turtle::new();
run_turtle!(|mut turtle| {
println!("Turn using the left and right arrow keys");
@@ -40,4 +39,4 @@ fn main() {
}
}
}
}
});
Copy path View file
@@ -1,14 +1,11 @@
#[macro_use]
extern crate turtle;
use turtle::Turtle;
fn main() {
let mut turtle = Turtle::new();
run_turtle!(|mut turtle| {
for _ in 0..360 {
// Move forward three steps
turtle.forward(3.0);
// Rotate to the right (clockwise) by 1 degree
turtle.right(1.0);
}
}
});
Copy path View file
@@ -1,12 +1,11 @@
#[macro_use]
extern crate turtle;
use std::f64::consts::E;
use turtle::{Turtle, Color, random};
fn main() {
let mut turtle = Turtle::new();
use turtle::Color;
run_turtle!(|mut turtle| {
let amplitude = 100.0;
let width = 800.0;
let step = 2.0;
@@ -27,8 +26,9 @@ fn main() {
// 200e^(-(1/200(x - 400))^2)
let y = amplitude * E.powf(-(1.0/(width / 4.0) * (x - width/2.0)).powi(2));
turtle.set_pen_color(random::<Color>().opaque());
let pen_color = turtle.random::<Color>().opaque();
turtle.set_pen_color(pen_color);
turtle.set_pen_size(y * height_factor);
turtle.forward(step);
}
}
});
Copy path View file
@@ -34,13 +34,13 @@
//! function, passing the direction of the turn for this fold as an angle
//! (+90 for a right turn, -90 for a left turn).
#[macro_use]
extern crate turtle;
use turtle::Turtle;
use turtle::Color;
fn main() {
let mut turtle = Turtle::new();
run_turtle!(|mut turtle| {
turtle.drawing_mut().set_background_color("#112244");
@@ -54,7 +54,7 @@ fn main() {
dragon(&mut turtle, -90., 11, 0., 255.);
turtle.hide();
}
});
/// Draw the dragon curve by simulating folding a strip of paper
///
@@ -74,7 +74,7 @@ fn dragon(
if num_folds == 0 {
// mapping a color number 0-255 to an rgb gradient.
turtle.set_pen_color(Color {
red: (color_mid - 128.).abs() * 2.,
red: (color_mid - 127.5).abs() * 2.,

This comment has been minimized.

@marshallpierce

marshallpierce Dec 31, 2017

this avoids a crash as the recently added color checking logic does its thing

green: color_mid,
blue: 160.,
alpha: 1.,
Copy path View file
@@ -1,9 +1,7 @@
#[macro_use]
extern crate turtle;
use turtle::Turtle;
fn main() {
let mut turtle = Turtle::new();
run_turtle!(|mut turtle| {
let points = 5.0;
let angle = 180.0 / points;
@@ -23,4 +21,4 @@ fn main() {
turtle.forward(100.0);
turtle.right(180.0 - angle);
}
}
});
Copy path View file
@@ -1,10 +1,9 @@
#[macro_use]
extern crate turtle;
use turtle::Turtle;
use turtle::Event::MouseMove;
fn main() {
let mut turtle = Turtle::new();
run_turtle!(|mut turtle| {
println!("Move your mouse around the window");
@@ -24,4 +23,4 @@ fn main() {
target = [x, y];
}
}
}
});
Copy path View file
@@ -1,9 +1,9 @@
#[macro_use]
extern crate turtle;
use turtle::Turtle;
fn main() {
let mut turtle = Turtle::new();
run_turtle!(|mut turtle| {
turtle.set_speed(5);
turtle.set_pen_size(3.0);
@@ -28,7 +28,7 @@ fn main() {
end_loop(&mut turtle);
turtle.drawing_mut().set_background_color("pink");
}
});
fn curve(turtle: &mut Turtle) {
for _ in 0..100 {
Copy path View file
@@ -1,16 +1,15 @@
//! To run a LOGO program, run:
//! cargo run --example logo_interpreter -- my_logo_program.txt
#[macro_use]
extern crate turtle;
use turtle::Turtle;
use std::env;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn main() {
let mut turtle = Turtle::new();
run_turtle!(|mut turtle| {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
@@ -23,7 +22,7 @@ fn main() {
let file = File::open(path).expect("Could not open provided program");
interpret(&mut turtle, file)
}
}
});
fn interpret<R: io::Read>(turtle: &mut Turtle, input: R) {
let mut reader = BufReader::new(input);
Copy path View file
@@ -1,7 +1,5 @@
use std::ops::{Index, IndexMut};
use turtle::random;
use cell::Cell;
use wall::Wall;
@@ -161,16 +159,6 @@ impl Grid {
Grid([Cells::default(); GRID_SIZE])
}
pub fn random_walls() -> Grid {
let mut grid = [Cells::default(); GRID_SIZE];
for row in &mut grid {
for cell in row {
*cell = random();
}
}
Grid(grid)
}
/// Returns true if there is NO wall between two adjacent cells
pub fn is_open_between(
&self,
Copy path View file
@@ -4,6 +4,7 @@ mod maze;
mod grid;
mod solver;
#[macro_use]
extern crate turtle;
use turtle::{Turtle, Color};
@@ -16,12 +17,10 @@ use solver::solve;
const WIDTH: f64 = 600.0; // px
const HEIGHT: f64 = 600.0; // px
fn main() {
turtle::start();
run_turtle!(|mut turtle| {
let maze = Maze::generate();
let maze = maze::generate(&mut turtle.rng());
let mut turtle = Turtle::new();
turtle.set_speed(8);
turtle.drawing_mut().set_background_color("#BDBDBD");
turtle.set_pen_color("#03A9F4");
@@ -74,7 +73,7 @@ fn main() {
turtle.set_speed(5);
turtle.set_pen_size(2.0);
solve(&mut turtle, maze, cell_width, cell_height);
}
});
fn draw_rows<
'a,
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.