diff --git a/examples/rust-wasm/.gitignore b/examples/rust-wasm/.gitignore new file mode 100644 index 0000000..27e49ac --- /dev/null +++ b/examples/rust-wasm/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +/coverage + +# next.js +.next/ +out/ + +# production +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Rust +crate/pkg +crate/target diff --git a/examples/rust-wasm/.prettierrc b/examples/rust-wasm/.prettierrc new file mode 100644 index 0000000..9aa793a --- /dev/null +++ b/examples/rust-wasm/.prettierrc @@ -0,0 +1,6 @@ +{ + "bracketSpacing": true, + "singleQuote": false, + "trailingComma": "all", + "arrowParens": "avoid" +} diff --git a/examples/rust-wasm/Dockerfile b/examples/rust-wasm/Dockerfile new file mode 100644 index 0000000..fc937a2 --- /dev/null +++ b/examples/rust-wasm/Dockerfile @@ -0,0 +1,24 @@ +FROM rustlang/rust:nightly + +# Install node +RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \ + && apt-get install -y nodejs \ + && npm i -g yarn + +# Install wasm-pack +RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + +# Create app directory +WORKDIR /usr/src/app + +# Bundle app source +COPY . . + +# Install deps +RUN yarn + +# Build +RUN yarn build + +# Start +CMD [ "yarn", "start" ] diff --git a/examples/rust-wasm/README.md b/examples/rust-wasm/README.md new file mode 100644 index 0000000..63a83b6 --- /dev/null +++ b/examples/rust-wasm/README.md @@ -0,0 +1,38 @@ +--- +title: WASM Rust +description: Game of life implemented in Rust and served with NextJS +tags: + - wasm + - rust + - next + - typescript + - tailwind +--- + +# WebAssembly Rust + +This example is a implements [Conway's Game of +Life](https://rustwasm.github.io/book/game-of-life/introduction.html) tutorial +in Rust and WebAssembly. The frontend is served as a +[NextJS](https://nextjs.org/) app with TypeScript and Tailwind. + +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new?template=https%3A%2F%2Fgithub.com%2Frailwayapp%2Fexamples%2Ftree%2Fmaster%2Fexamples%2Frust-wasm) + +## ✨ Features + +- Rust +- WASM +- TypeScript +- NextJS +- Tailwind + +## 💁‍♀️ How to use + +- Ensure you have the [Rust toolchain](https://www.rust-lang.org/) setup +- Install [wasm-pack](https://rustwasm.github.io/wasm-pack/) +- Install deps with `yarn` +- Run app in development `yarn dev` + +## 📝 Notes + +This starter implements part of the official [Rust WASM tutorial](https://rustwasm.github.io/book/game-of-life/introduction.html). diff --git a/examples/rust-wasm/package.json b/examples/rust-wasm/package.json new file mode 100644 index 0000000..de22869 --- /dev/null +++ b/examples/rust-wasm/package.json @@ -0,0 +1,25 @@ +{ + "name": "wasm-starter", + "private": true, + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "scripts": { + "dev": "yarn build:wasm && concurrently -n crate,www -c magenta,cyan \"yarn dev:www\" \"yarn dev:wasm\" ", + "dev:www": "yarn workspace www dev", + "dev:wasm": "yarn workspace crate dev", + "build": "yarn build:wasm && yarn build:www", + "build:www": "yarn workspace www build", + "build:wasm": "yarn workspace crate build", + "start": "yarn workspace www start", + "clean": "wsrun --exclude-missing clean" + }, + "devDependencies": { + "concurrently": "^6.1.0", + "wsrun": "^5.2.4", + "prettier": "^2.2.1" + } +} diff --git a/examples/rust-wasm/packages/crate/.gitignore b/examples/rust-wasm/packages/crate/.gitignore new file mode 100644 index 0000000..4e30131 --- /dev/null +++ b/examples/rust-wasm/packages/crate/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/examples/rust-wasm/packages/crate/Cargo.toml b/examples/rust-wasm/packages/crate/Cargo.toml new file mode 100644 index 0000000..9c44da0 --- /dev/null +++ b/examples/rust-wasm/packages/crate/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "crate" +version = "0.1.0" +authors = ["Jake Runzer "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2" +js-sys = "0.3.51" +cargo-watch = "7.8.0" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.1", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.2", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.2" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/examples/rust-wasm/packages/crate/LICENSE_MIT b/examples/rust-wasm/packages/crate/LICENSE_MIT new file mode 100644 index 0000000..ba45a49 --- /dev/null +++ b/examples/rust-wasm/packages/crate/LICENSE_MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018 Jake Runzer + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/examples/rust-wasm/packages/crate/README.md b/examples/rust-wasm/packages/crate/README.md new file mode 100644 index 0000000..1e4617a --- /dev/null +++ b/examples/rust-wasm/packages/crate/README.md @@ -0,0 +1,69 @@ +
+ +

wasm-pack-template

+ + A template for kick starting a Rust and WebAssembly project using wasm-pack. + +

+ Build Status +

+ +

+ Tutorial + | + Chat +

+ + Built with 🦀🕸 by The Rust and WebAssembly Working Group +
+ +## About + +[**📚 Read this template tutorial! 📚**][template-docs] + +This template is designed for compiling Rust libraries into WebAssembly and +publishing the resulting package to NPM. + +Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other +templates and usages of `wasm-pack`. + +[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html +[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html + +## 🚴 Usage + +### 🐑 Use `cargo generate` to Clone this Template + +[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) + +``` +cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project +cd my-project +``` + +### 🛠️ Build with `wasm-pack build` + +``` +wasm-pack build +``` + +### 🔬 Test in Headless Browsers with `wasm-pack test` + +``` +wasm-pack test --headless --firefox +``` + +### 🎁 Publish to NPM with `wasm-pack publish` + +``` +wasm-pack publish +``` + +## 🔋 Batteries Included + +* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating + between WebAssembly and JavaScript. +* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) + for logging panic messages to the developer console. +* [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized + for small code size. diff --git a/examples/rust-wasm/packages/crate/package.json b/examples/rust-wasm/packages/crate/package.json new file mode 100644 index 0000000..fa588a3 --- /dev/null +++ b/examples/rust-wasm/packages/crate/package.json @@ -0,0 +1,13 @@ +{ + "name": "crate", + "private": true, + "version": "0.0.1", + "author": "Jake Runzer ", + "license": "MIT", + "main": "pkg/crate.js", + "scripts": { + "dev": "cargo watch -i .gitignore -i 'pkg/*' -s 'wasm-pack build'", + "build": "wasm-pack build", + "clean": "cargo clean && rm -rf pkg" + } +} diff --git a/examples/rust-wasm/packages/crate/src/lib.rs b/examples/rust-wasm/packages/crate/src/lib.rs new file mode 100644 index 0000000..eb7080d --- /dev/null +++ b/examples/rust-wasm/packages/crate/src/lib.rs @@ -0,0 +1,168 @@ +mod utils; + +use std::fmt; + +use js_sys::Math::random; +use wasm_bindgen::prelude::*; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +extern "C" { + fn alert(s: &str); +} + +#[wasm_bindgen] +pub fn value() -> String { + "Hello from Rust!".into() +} + +#[wasm_bindgen] +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Cell { + Dead = 0, + Alive = 1, +} + +#[wasm_bindgen] +pub struct Universe { + width: u32, + height: u32, + cells: Vec, +} + +impl Cell { + fn toggle(&mut self) { + *self = match *self { + Cell::Dead => Cell::Alive, + Cell::Alive => Cell::Dead, + } + } +} + +#[wasm_bindgen] +impl Universe { + pub fn new() -> Universe { + let width = 64; + let height = 64; + + let cells = (0..width * height).map(|_i| new_cell()).collect(); + + Universe { + width, + height, + cells, + } + } + + pub fn toggle_cell(&mut self, row: u32, column: u32) { + let idx = self.get_index(row, column); + self.cells[idx].toggle() + } + + pub fn restart(&mut self) { + self.cells = (0..self.width * self.height).map(|_i| new_cell()).collect(); + } + + pub fn render(&self) -> String { + self.to_string() + } + + pub fn width(&self) -> u32 { + self.width + } + + pub fn height(&self) -> u32 { + self.height + } + + pub fn cells(&self) -> *const Cell { + self.cells.as_ptr() + } + + fn get_index(&self, row: u32, column: u32) -> usize { + (row * self.width + column) as usize + } + + fn live_neighbor_count(&self, row: u32, column: u32) -> u8 { + let mut count = 0; + for delta_row in [self.height - 1, 0, 1].iter().cloned() { + for delta_col in [self.width - 1, 0, 1].iter().cloned() { + if delta_row == 0 && delta_col == 0 { + continue; + } + + let neighbor_row = (row + delta_row) % self.height; + let neighbor_col = (column + delta_col) % self.height; + let idx = self.get_index(neighbor_row, neighbor_col); + count += self.cells[idx] as u8; + } + } + + count + } + + pub fn tick(&mut self) { + let mut next = self.cells.clone(); + + for row in 0..self.height { + for col in 0..self.width { + let idx = self.get_index(row, col); + let cell = self.cells[idx]; + let live_neighbors = self.live_neighbor_count(row, col); + + let next_cell = match (cell, live_neighbors) { + // Rule 1: Any live cell with fewer than two live neighbours + // dies, as if caused by underpopulation. + (Cell::Alive, x) if x < 2 => Cell::Dead, + + // Rule 2: Any live cell with two or three live neighbours + // lives on to the next generation. + (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive, + + // Rule 3: Any live cell with more than three live + // neighbours dies, as if by overpopulation. + (Cell::Alive, x) if x > 3 => Cell::Dead, + + // Rule 4: Any dead cell with exactly three live neighbours + // becomes a live cell, as if by reproduction. + (Cell::Dead, 3) => Cell::Alive, + + // All other cells remain in the same state. + (otherwise, _) => otherwise, + }; + + next[idx] = next_cell; + } + } + + self.cells = next; + } +} + +fn new_cell() -> Cell { + if random() < 0.5 { + Cell::Alive + } else { + Cell::Dead + } +} + +impl fmt::Display for Universe { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for line in self.cells.as_slice().chunks(self.width as usize) { + for &cell in line { + let symbol = if cell == Cell::Dead { '◻' } else { '◼' }; + write!(f, "{}", symbol)?; + } + write!(f, "\n")?; + } + + Ok(()) + } +} diff --git a/examples/rust-wasm/packages/crate/src/utils.rs b/examples/rust-wasm/packages/crate/src/utils.rs new file mode 100644 index 0000000..b1d7929 --- /dev/null +++ b/examples/rust-wasm/packages/crate/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/examples/rust-wasm/packages/crate/tests/web.rs b/examples/rust-wasm/packages/crate/tests/web.rs new file mode 100644 index 0000000..de5c1da --- /dev/null +++ b/examples/rust-wasm/packages/crate/tests/web.rs @@ -0,0 +1,13 @@ +//! Test suite for the Web and headless browsers. + +#![cfg(target_arch = "wasm32")] + +extern crate wasm_bindgen_test; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn pass() { + assert_eq!(1 + 1, 2); +} diff --git a/examples/rust-wasm/packages/www/.babelrc.js b/examples/rust-wasm/packages/www/.babelrc.js new file mode 100644 index 0000000..52ad7bd --- /dev/null +++ b/examples/rust-wasm/packages/www/.babelrc.js @@ -0,0 +1,4 @@ +module.exports = { + presets: [["next/babel", { "preset-react": { runtime: "automatic" } }]], + plugins: ["babel-plugin-macros", ["styled-components", { ssr: true }]], +}; diff --git a/examples/rust-wasm/packages/www/.gitignore b/examples/rust-wasm/packages/www/.gitignore new file mode 100644 index 0000000..cdae205 --- /dev/null +++ b/examples/rust-wasm/packages/www/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +.blog_index_data +.blog_index_data_previews diff --git a/examples/rust-wasm/packages/www/LICENSE b/examples/rust-wasm/packages/www/LICENSE new file mode 100644 index 0000000..73450b9 --- /dev/null +++ b/examples/rust-wasm/packages/www/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 Jake Runzer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/examples/rust-wasm/packages/www/README.md b/examples/rust-wasm/packages/www/README.md new file mode 100644 index 0000000..e5d51e8 --- /dev/null +++ b/examples/rust-wasm/packages/www/README.md @@ -0,0 +1,21 @@ +# NextJS Starter + +This is my goto starter for new NextJS projects. It has + +- [Typescript](https://www.typescriptlang.org/) +- [Tailwind](https://tailwindcss.com/) + [Twin](https://github.com/ben-rogerson/twin.macro) for styling +- Light/dark modes +- [Fathom](https://usefathom.com/) for analytics +- Simple page/layout structure + +## Development + +This is a [Next.js](https://nextjs.org/) site + +```bash +# Install deps +yarn + +# Start development server +yarn dev +``` diff --git a/examples/rust-wasm/packages/www/next-env.d.ts b/examples/rust-wasm/packages/www/next-env.d.ts new file mode 100644 index 0000000..7b7aa2c --- /dev/null +++ b/examples/rust-wasm/packages/www/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/rust-wasm/packages/www/next.config.js b/examples/rust-wasm/packages/www/next.config.js new file mode 100644 index 0000000..ffcac55 --- /dev/null +++ b/examples/rust-wasm/packages/www/next.config.js @@ -0,0 +1,11 @@ +module.exports = { + webpack: (config, { isServer }) => { + if (!isServer) { + // Unset client-side javascript that only works server-side + // https://github.com/vercel/next.js/issues/7755#issuecomment-508633125 + config.node = { fs: "empty", module: "empty" }; + } + + return config; + }, +}; diff --git a/examples/rust-wasm/packages/www/package.json b/examples/rust-wasm/packages/www/package.json new file mode 100644 index 0000000..0a53499 --- /dev/null +++ b/examples/rust-wasm/packages/www/package.json @@ -0,0 +1,42 @@ +{ + "name": "www", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start --port ${PORT-3000}", + "tsc": "tsc -p .", + "clean": "rm -rf .next" + }, + "dependencies": { + "crate": "0.0.1", + "next": "10.0.9", + "next-seo": "^4.23.0", + "next-themes": "^0.0.14", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-feather": "^2.0.9", + "styled-components": "^5.2.1", + "theme-custom-properties": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^14.14.37", + "@types/react": "^17.0.3", + "@types/styled-components": "^5.1.9", + "autoprefixer": "^10.2.5", + "babel-plugin-macros": "^3.0.1", + "babel-plugin-styled-components": "^1.12.0", + "postcss": "^8.2.8", + "react-is": "^17.0.2", + "tailwindcss": "^2.0.4", + "ts-node": "^9.1.1", + "twin.macro": "^2.3.1", + "typescript": "^4.2.3" + }, + "babelMacros": { + "twin": { + "preset": "styled-components" + } + } +} diff --git a/examples/rust-wasm/packages/www/postcss.config.js b/examples/rust-wasm/packages/www/postcss.config.js new file mode 100644 index 0000000..f46b31c --- /dev/null +++ b/examples/rust-wasm/packages/www/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["tailwindcss", "autoprefixer"], +}; diff --git a/examples/rust-wasm/packages/www/public/android-chrome-192x192.png b/examples/rust-wasm/packages/www/public/android-chrome-192x192.png new file mode 100644 index 0000000..25b029e Binary files /dev/null and b/examples/rust-wasm/packages/www/public/android-chrome-192x192.png differ diff --git a/examples/rust-wasm/packages/www/public/android-chrome-512x512.png b/examples/rust-wasm/packages/www/public/android-chrome-512x512.png new file mode 100644 index 0000000..e2e146d Binary files /dev/null and b/examples/rust-wasm/packages/www/public/android-chrome-512x512.png differ diff --git a/examples/rust-wasm/packages/www/public/apple-touch-icon.png b/examples/rust-wasm/packages/www/public/apple-touch-icon.png new file mode 100644 index 0000000..34d828f Binary files /dev/null and b/examples/rust-wasm/packages/www/public/apple-touch-icon.png differ diff --git a/examples/rust-wasm/packages/www/public/favicon-16x16.png b/examples/rust-wasm/packages/www/public/favicon-16x16.png new file mode 100644 index 0000000..3f00215 Binary files /dev/null and b/examples/rust-wasm/packages/www/public/favicon-16x16.png differ diff --git a/examples/rust-wasm/packages/www/public/favicon-32x32.png b/examples/rust-wasm/packages/www/public/favicon-32x32.png new file mode 100644 index 0000000..49949ab Binary files /dev/null and b/examples/rust-wasm/packages/www/public/favicon-32x32.png differ diff --git a/examples/rust-wasm/packages/www/public/favicon.ico b/examples/rust-wasm/packages/www/public/favicon.ico new file mode 100644 index 0000000..a1c1c25 Binary files /dev/null and b/examples/rust-wasm/packages/www/public/favicon.ico differ diff --git a/examples/rust-wasm/packages/www/public/site.webmanifest b/examples/rust-wasm/packages/www/public/site.webmanifest new file mode 100644 index 0000000..a0f259b --- /dev/null +++ b/examples/rust-wasm/packages/www/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Jake", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#e41b61", + "background_color": "#f8f9fa", + "display": "standalone" +} diff --git a/examples/rust-wasm/packages/www/src/components/Life.tsx b/examples/rust-wasm/packages/www/src/components/Life.tsx new file mode 100644 index 0000000..f138952 --- /dev/null +++ b/examples/rust-wasm/packages/www/src/components/Life.tsx @@ -0,0 +1,177 @@ +import { Cell, Universe } from "crate"; +import { memory } from "crate/pkg/crate_bg.wasm"; +import React, { useEffect, useRef, useState } from "react"; +import { Play, Pause, RefreshCw } from "react-feather"; +import tw from "twin.macro"; +import { Link } from "./Link"; + +const CELL_SIZE = 10; // px +const GRID_COLOR = "#CCCCCC"; +const DEAD_COLOR = "#FFFFFF"; +const ALIVE_COLOR = "#000000"; + +const Life: React.FC = () => { + const canvasRef = useRef(null); + const universeRef = useRef(Universe.new()); + const [isPaused, setIsPaused] = useState(false); + + const drawGrid = ( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + ) => { + ctx.beginPath(); + ctx.strokeStyle = GRID_COLOR; + + // Vertical lines. + for (let i = 0; i <= width; i++) { + ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0); + ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1); + } + + // Horizontal lines. + for (let j = 0; j <= height; j++) { + ctx.moveTo(0, j * (CELL_SIZE + 1) + 1); + ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1); + } + + ctx.stroke(); + }; + + const getIndex = (row: number, column: number, width: number) => { + return row * width + column; + }; + + const drawCells = ( + ctx: CanvasRenderingContext2D, + universe: Universe, + width: number, + height: number, + ) => { + const cellsPtr = universe.cells(); + const cells = new Uint8Array(memory.buffer, cellsPtr, width * height); + + ctx.beginPath(); + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const idx = getIndex(row, col, width); + + ctx.fillStyle = cells[idx] === Cell.Dead ? DEAD_COLOR : ALIVE_COLOR; + + ctx.fillRect( + col * (CELL_SIZE + 1) + 1, + row * (CELL_SIZE + 1) + 1, + CELL_SIZE, + CELL_SIZE, + ); + } + } + + ctx.stroke(); + }; + + useEffect(() => { + const canvas = canvasRef.current; + const ctx = canvas?.getContext("2d"); + if (canvas == null || ctx == null) return; + + const universe = universeRef.current; + const width = universe.width(); + const height = universe.height(); + + canvas.height = (CELL_SIZE + 1) * height + 1; + canvas.width = (CELL_SIZE + 1) * width + 1; + + let animationId: number | null = null; + + const renderLoop = () => { + if (!isPaused) { + universe.tick(); + } + + drawGrid(ctx, width, height); + drawCells(ctx, universe, width, height); + + animationId = requestAnimationFrame(renderLoop); + }; + + renderLoop(); + + return () => { + if (animationId != null) { + cancelAnimationFrame(animationId); + } + }; + }, [isPaused]); + + const onClickCell = (x: number, y: number) => { + const canvas = canvasRef.current; + const ctx = canvas?.getContext("2d"); + if (canvas == null || ctx == null) return; + + const universe = universeRef.current; + const width = universe.width(); + const height = universe.height(); + + const boundingRect = canvas.getBoundingClientRect(); + + const scaleX = canvas.width / boundingRect.width; + const scaleY = canvas.height / boundingRect.height; + + const canvasLeft = (x - boundingRect.left) * scaleX; + const canvasTop = (y - boundingRect.top) * scaleY; + + const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1); + const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1); + + universe.toggle_cell(row, col); + + drawGrid(ctx, width, height); + drawCells(ctx, universe, width, height); + }; + + return ( +
+
+
+

Game of Life

+

+ Implemented in Rust based off the{" "} + + Wasm Game of Life tutorial + +

+
+ +
+ + + +
+
+ + onClickCell(e.clientX, e.clientY)} + /> +
+ ); +}; + +export default Life; diff --git a/examples/rust-wasm/packages/www/src/components/Link.tsx b/examples/rust-wasm/packages/www/src/components/Link.tsx new file mode 100644 index 0000000..37fdae2 --- /dev/null +++ b/examples/rust-wasm/packages/www/src/components/Link.tsx @@ -0,0 +1,38 @@ +import NLink from "next/link"; +import React, { useMemo } from "react"; +import "twin.macro"; + +export interface Props { + href: string; + external?: boolean; + className?: string; +} + +const isExternalLink = (href: string) => + href == null || href.startsWith("http://") || href.startsWith("https://"); + +const useIsExternalLink = (href: string) => + useMemo(() => isExternalLink(href), [href]); + +export const Link: React.FC = ({ + href, + external, + children, + ...props +}) => { + const isExternal = (useIsExternalLink(href) || external) ?? false; + + if (isExternal) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +}; diff --git a/examples/rust-wasm/packages/www/src/components/SEO.tsx b/examples/rust-wasm/packages/www/src/components/SEO.tsx new file mode 100644 index 0000000..c7dae7e --- /dev/null +++ b/examples/rust-wasm/packages/www/src/components/SEO.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import { DefaultSeo, NextSeo, NextSeoProps } from "next-seo"; +import Head from "next/head"; +import { DefaultSeoProps } from "next-seo"; + +export interface Props extends NextSeoProps { + title?: string; + description?: string; + image?: string; +} + +const title = "WASM Starter"; +export const url = ""; +const description = "Game of Life implemented in Rust"; +const image = ""; + +const config: DefaultSeoProps = { + title, + description, + openGraph: { + type: "website", + url, + site_name: title, + images: [{ url: image }], + }, +}; + +export const SEO: React.FC = ({ image, ...props }) => { + const title = props.title ?? config.title; + const description = props.description || config.description; + + return ( + <> + + + + + + {title} + + + + + ); +}; diff --git a/examples/rust-wasm/packages/www/src/components/Text.tsx b/examples/rust-wasm/packages/www/src/components/Text.tsx new file mode 100644 index 0000000..01aaadf --- /dev/null +++ b/examples/rust-wasm/packages/www/src/components/Text.tsx @@ -0,0 +1,26 @@ +import { Link } from "./Link"; +import tw from "twin.macro"; + +export const Header = tw.h1` + text-4xl font-extrabold mb-8 +`; + +export const SubHeader = tw.h2` + text-2xl font-bold mb-4 mt-8 +`; + +export const Paragraph = tw.p` + mb-12 max-w-sm text-article +`; + +export const PillLink = tw(Link)` + block text-fg rounded-sm px-1 bg-accentDim hover:bg-accent hover:text-bg dark:hover:text-fg +`; + +export const HighlightLink = tw(Link)` + font-bold hover:text-accent +`; + +export const UnderlineLink = tw(HighlightLink)` + underline +`; diff --git a/examples/rust-wasm/packages/www/src/layouts/Page.tsx b/examples/rust-wasm/packages/www/src/layouts/Page.tsx new file mode 100644 index 0000000..5d0c75e --- /dev/null +++ b/examples/rust-wasm/packages/www/src/layouts/Page.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { Props as SEOProps, SEO } from "../components/SEO"; +import "twin.macro"; + +export interface Props { + seo?: SEOProps; +} + +export const Page: React.FC = props => { + return ( + <> + + +
+
+ {props.children} +
+
+ + ); +}; diff --git a/examples/rust-wasm/packages/www/src/pages/_app.tsx b/examples/rust-wasm/packages/www/src/pages/_app.tsx new file mode 100644 index 0000000..b9dd36a --- /dev/null +++ b/examples/rust-wasm/packages/www/src/pages/_app.tsx @@ -0,0 +1,16 @@ +import { AppProps } from "next/app"; +import React from "react"; +import { GlobalStyles } from "../styles/GlobalStyles"; +import { TwinGlobalStyles } from "../styles/TwinGlobalStyles"; + +const MyApp = ({ Component, pageProps }: AppProps) => { + return ( + <> + + + + + ); +}; + +export default MyApp; diff --git a/examples/rust-wasm/packages/www/src/pages/_document.tsx b/examples/rust-wasm/packages/www/src/pages/_document.tsx new file mode 100644 index 0000000..72476ab --- /dev/null +++ b/examples/rust-wasm/packages/www/src/pages/_document.tsx @@ -0,0 +1,28 @@ +import Document from "next/document"; +import { ServerStyleSheet } from "styled-components"; + +export default class MyDocument extends Document { + static async getInitialProps(ctx) { + const sheet = new ServerStyleSheet(); + const originalRenderPage = ctx.renderPage; + try { + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: App => props => sheet.collectStyles(), + }); + const initialProps = await Document.getInitialProps(ctx); + + return { + ...initialProps, + styles: ( + <> + {initialProps.styles} + {sheet.getStyleElement()} + + ), + }; + } finally { + sheet.seal(); + } + } +} diff --git a/examples/rust-wasm/packages/www/src/pages/index.tsx b/examples/rust-wasm/packages/www/src/pages/index.tsx new file mode 100644 index 0000000..798e855 --- /dev/null +++ b/examples/rust-wasm/packages/www/src/pages/index.tsx @@ -0,0 +1,17 @@ +import { NextPage } from "next"; +import dynamic from "next/dynamic"; +import React from "react"; +import "twin.macro"; +import { Page } from "../layouts/Page"; + +const DynamicLife = dynamic(() => import("../components/Life")); + +const Home: NextPage = () => { + return ( + + + + ); +}; + +export default Home; diff --git a/examples/rust-wasm/packages/www/src/styles/GlobalStyles.ts b/examples/rust-wasm/packages/www/src/styles/GlobalStyles.ts new file mode 100644 index 0000000..23f34a5 --- /dev/null +++ b/examples/rust-wasm/packages/www/src/styles/GlobalStyles.ts @@ -0,0 +1,10 @@ +import { createGlobalStyle } from "styled-components"; +import tw from "twin.macro"; + +export const GlobalStyles = createGlobalStyle` + body { + ${tw`antialiased`} + ${tw`text-black bg-gray-50 leading-relaxed`} + ${tw`font-sans`} + } +`; diff --git a/examples/rust-wasm/packages/www/src/styles/TwinGlobalStyles.tsx b/examples/rust-wasm/packages/www/src/styles/TwinGlobalStyles.tsx new file mode 100644 index 0000000..05d0bf3 --- /dev/null +++ b/examples/rust-wasm/packages/www/src/styles/TwinGlobalStyles.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import { GlobalStyles } from "twin.macro"; + +export const TwinGlobalStyles: React.FC = () => { + return ; +}; diff --git a/examples/rust-wasm/packages/www/tailwind.config.js b/examples/rust-wasm/packages/www/tailwind.config.js new file mode 100644 index 0000000..6c0b592 --- /dev/null +++ b/examples/rust-wasm/packages/www/tailwind.config.js @@ -0,0 +1,15 @@ +const colors = require("tailwindcss/colors"); + +module.exports = { + purge: ["./src/**/*.{js,ts,jsx,tsx}"], + darkMode: "class", // 'media' or 'class' + theme: { + fontFamily: { + sans: `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`, + }, + colors: colors, + }, + variants: { + extend: {}, + }, +}; diff --git a/examples/rust-wasm/packages/www/tsconfig.json b/examples/rust-wasm/packages/www/tsconfig.json new file mode 100644 index 0000000..0833bf8 --- /dev/null +++ b/examples/rust-wasm/packages/www/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noImplicitAny": false, + "forceConsistentCasingInFileNames": true, + "downlevelIteration": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/rust-wasm/packages/www/typings/twin.d.ts b/examples/rust-wasm/packages/www/typings/twin.d.ts new file mode 100644 index 0000000..315f6c1 --- /dev/null +++ b/examples/rust-wasm/packages/www/typings/twin.d.ts @@ -0,0 +1,38 @@ +// twin.d.ts +import "twin.macro"; +import styledImport, { CSSProp, css as cssImport } from "styled-components"; +import {} from "styled-components/cssprop"; + +declare module "twin.macro" { + // The styled and css imports + const styled: typeof styledImport; + const css: typeof cssImport; +} + +declare module "react" { + // The css prop + interface HTMLAttributes extends DOMAttributes { + css?: CSSProp; + tw?: string; + } + + // The inline svg css prop + interface SVGProps extends SVGProps { + css?: CSSProp; + } + + //