├── tests └── app.rs ├── .eslintignore ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .npmignore ├── publish.js ├── core ├── timer.rs ├── utils.rs └── lib.rs ├── example ├── webpack.config.js ├── index.js └── index.html ├── tsconfig.json ├── src ├── wasm.ts └── index.ts ├── LICENSE ├── .eslintrc ├── Cargo.toml ├── rollup.config.js ├── README.md ├── package.json └── Cargo.lock /tests/app.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | pkg 4 | target 5 | example/build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | target 4 | pkg 5 | wasm-pack.log 6 | example/build -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "tabWidth": 2 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Cargo.* 2 | src/**.* 3 | **/*.rs 4 | tsconfig.json 5 | yarn.lock 6 | pkg 7 | target 8 | node_modules 9 | core -------------------------------------------------------------------------------- /publish.js: -------------------------------------------------------------------------------- 1 | const ghpages = require("gh-pages"); 2 | 3 | ghpages.publish("example/build", function (err) { 4 | if (err) throw err; 5 | console.log("✨ Published to Github!"); 6 | }); 7 | -------------------------------------------------------------------------------- /core/timer.rs: -------------------------------------------------------------------------------- 1 | extern crate web_sys; 2 | use web_sys::console; 3 | 4 | pub struct Timer<'a> { 5 | name: &'a str, 6 | } 7 | 8 | impl<'a> Timer<'a> { 9 | pub fn new(name: &'a str) -> Timer<'a> { 10 | console::time_with_label(name); 11 | Timer { name } 12 | } 13 | } 14 | 15 | impl<'a> Drop for Timer<'a> { 16 | fn drop(&mut self) { 17 | console::time_end_with_label(self.name); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: "./index.js", 7 | output: { 8 | path: path.resolve(__dirname, "build"), 9 | filename: "index.js", 10 | }, 11 | mode: process.env.NODE_ENV || "development", 12 | plugins: [new CopyWebpackPlugin({ patterns: ["index.html"] })], 13 | stats: "errors-only", 14 | }; 15 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { GameOfLife } from "../dist"; 2 | 3 | (async () => { 4 | const game = new GameOfLife(); 5 | await game.initialize(); 6 | 7 | const button = document.getElementById("start"); 8 | 9 | button.addEventListener("click", () => { 10 | if (game.paused) { 11 | button.textContent = "pause"; 12 | game.resume(); 13 | } else { 14 | button.textContent = "start"; 15 | game.pause(); 16 | } 17 | }); 18 | })(); 19 | -------------------------------------------------------------------------------- /core/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn set_panic_hook() { 2 | // When the `console_error_panic_hook` feature is enabled, we can call the 3 | // `set_panic_hook` function at least once during initialization, and then 4 | // we will get better error messages if our code ever panics. 5 | // 6 | // For more details see 7 | // https://github.com/rustwasm/console_error_panic_hook#readme 8 | #[cfg(feature = "console_error_panic_hook")] 9 | console_error_panic_hook::set_once(); 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "declarationDir": "dist", 7 | "esModuleInterop": true, 8 | "outDir": "./dist", 9 | "lib": ["es6", "dom", "es2016", "es2017"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "rootDir": ".", 14 | "strict": true, 15 | "target": "es2018", 16 | "types": ["node"] 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": [ 20 | "**/node_modules", 21 | "**/build", 22 | "**/dist", 23 | "**/*.md", 24 | "**/rollup.config.js" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/wasm.ts: -------------------------------------------------------------------------------- 1 | import initWasm from "../pkg"; 2 | import wasm from "../pkg/index_bg.wasm"; 3 | 4 | function asciiToBinary(str: string) { 5 | if (typeof atob === "function") { 6 | return atob(str); 7 | } else { 8 | return new Buffer(str, "base64").toString("binary"); 9 | } 10 | } 11 | 12 | function decode(encoded: string) { 13 | const binaryString = asciiToBinary(encoded); 14 | const bytes = new Uint8Array(binaryString.length); 15 | for (let i = 0; i < binaryString.length; i++) { 16 | bytes[i] = binaryString.charCodeAt(i); 17 | } 18 | return bytes.buffer; 19 | } 20 | 21 | export async function init() { 22 | const program = await initWasm(decode(wasm as any)); 23 | return program; 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shaoru Ian Huang. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "import", "unused-imports"], 5 | "env": { 6 | "node": true, 7 | "browser": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:prettier/recommended" 14 | ], 15 | "rules": { 16 | "import/order": [ 17 | "error", 18 | { 19 | "alphabetize": { 20 | "order": "asc", 21 | "caseInsensitive": true 22 | }, 23 | "newlines-between": "always", 24 | "groups": ["builtin", "external", "parent", "sibling", "index"], 25 | "pathGroups": [ 26 | { 27 | "pattern": "react", 28 | "group": "external", 29 | "position": "before" 30 | } 31 | ], 32 | "pathGroupsExcludedImportTypes": ["builtin"] 33 | } 34 | ], 35 | // "unused-imports/no-unused-imports-ts": "warn", 36 | "@typescript-eslint/no-var-requires": "off", 37 | "@typescript-eslint/ban-ts-comment": "off", 38 | "prefer-const": "error", 39 | "prefer-template": "error" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TS + RS = ❤️! 7 | 8 | 12 | 13 | 37 | 38 | 39 | 43 | 44 |
45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # You must change these to your own details. 2 | [package] 3 | authors = ["Ian Huang "] 4 | categories = ["wasm"] 5 | description = "My awesome Rust, WebAssembly, and TypeScript library template!" 6 | edition = "2018" 7 | license = "MIT" 8 | name = "rust-typescript-template" 9 | readme = "README.md" 10 | repository = "https://github.com/iantheearl/rust-typescript-template" 11 | version = "0.1.0" 12 | 13 | [lib] 14 | crate-type = ["cdylib"] 15 | name = "core" 16 | path = "core/lib.rs" 17 | 18 | [profile.release] 19 | # This makes the compiled code faster and smaller, but it makes compiling slower, 20 | # so it's only enabled in release mode. 21 | lto = true 22 | 23 | [features] 24 | # If you uncomment this line, it will enable `wee_alloc`: 25 | #default = ["wee_alloc"] 26 | 27 | [dependencies] 28 | # The `wasm-bindgen` crate provides the bare minimum functionality needed 29 | # to interact with JavaScript. 30 | wasm-bindgen = "0.2.45" 31 | 32 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 33 | # compared to the default allocator's ~10K. However, it is slower than the default 34 | # allocator, so it's not enabled by default. 35 | wee_alloc = {version = "0.4.2", optional = true} 36 | 37 | fixedbitset = "0.4.1" 38 | 39 | # The `web-sys` crate allows you to interact with the various browser APIs, 40 | # like the DOM. 41 | [dependencies.web-sys] 42 | features = ["console"] 43 | version = "0.3.22" 44 | 45 | # These crates are used for running unit tests. 46 | [dev-dependencies] 47 | futures = "0.3.19" 48 | js-sys = "0.3.22" 49 | wasm-bindgen-futures = "0.4.28" 50 | wasm-bindgen-test = "0.3.28" 51 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | import resolve from "@rollup/plugin-node-resolve"; 5 | import { base64 } from "rollup-plugin-base64"; 6 | import nodePolyfills from "rollup-plugin-node-polyfills"; 7 | import { terser } from "rollup-plugin-terser"; 8 | import typescript from "rollup-plugin-typescript2"; 9 | 10 | const packageJson = require("./package.json"); 11 | 12 | const globals = { 13 | ...packageJson.devDependencies, 14 | }; 15 | 16 | export default { 17 | input: "src/index.ts", 18 | output: { 19 | file: packageJson.module, 20 | format: "esm", 21 | }, 22 | plugins: [ 23 | typescript({ 24 | useTsconfigDeclarationDir: true, 25 | }), 26 | nodePolyfills(), 27 | resolve({ 28 | browser: true, 29 | preferBuiltins: false, 30 | }), 31 | // used to load in .wasm files as base64 strings, then can be instantiated with 32 | // helper functions within `src/wasm.ts`. 33 | base64({ include: "**/*.wasm" }), 34 | { 35 | // copy over rust-built declaration files to dist for later .d.ts bundling 36 | name: "copy-pkg", 37 | generateBundle() { 38 | fs.mkdirSync(path.resolve(`dist/pkg`), { recursive: true }); 39 | 40 | ["index.d.ts", "index_bg.wasm.d.ts"].forEach((file) => { 41 | fs.copyFileSync( 42 | path.resolve(`./pkg/${file}`), 43 | path.resolve(`./dist/pkg/${file}`) 44 | ); 45 | }); 46 | 47 | fs.copyFileSync( 48 | path.resolve(`./pkg/index_bg.wasm`), 49 | path.resolve(`./dist/index_bg.wasm`) 50 | ); 51 | }, 52 | }, 53 | ...(!process.env.ROLLUP_WATCH ? [terser()] : []), 54 | ], 55 | external: [...Object.keys(globals)], 56 | watch: { clearScreen: false }, 57 | }; 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://i.imgur.com/lzXkRrw.jpg) 2 | 3 |

Create Rust + TypeScript libraries with ease! PR'S WELCOMED!

4 | 5 | ## :sparkles: Inspiration 6 | 7 | I wanted to create a WebAssembly/Rust library with additional JS features, but I couldn't find any resources on how to integrate the npm-ready library built by `wasm-pack` with TypeScript. All the examples online either seemed to use the output of `wasm-pack` using `npm link`, or didn't have TypeScript support. So, I created this, a template that allows you to embed `wasm-pack` all within one single TypeScript-based library. Enjoy! 8 | 9 | ## :building_construction: Install 10 | 11 | Before development, make sure you have installed: 12 | - [wasm-pack](https://github.com/rustwasm/wasm-pack) 13 | - [cargo-watch](https://github.com/watchexec/cargo-watch) 14 | 15 | ```bash 16 | # clone the repo 17 | git clone --depth 1 --branch master https://github.com/iantheearl/rust-typescript-template.git your-project-name 18 | cd your-project-name 19 | 20 | # install dependencies 21 | yarn 22 | 23 | # start development 24 | yarn run dev 25 | 26 | # run example 27 | yarn run example 28 | 29 | # visit http://localhost:8080 30 | ``` 31 | 32 | After installation, start developing here: 33 | - `./pkg`: Rust source files 34 | - `./src`: TypeScript source files 35 | 36 | ## :printer: Publish 37 | 38 | ```bash 39 | # build the library to ./dist, including .wasm 40 | yarn build 41 | 42 | # push to npm 43 | npm publish --access public 44 | ``` 45 | 46 | ## :tv: Deployment 47 | 48 | ```bash 49 | # build the example to `./example/build`, and push that folder to the gh-pages branch 50 | yarn deploy 51 | ``` 52 | 53 | Configure github to deploy branch gh-pages. 54 | 55 | 56 | ## :open_file_folder: Structure 57 | 58 | - `./core`: Rust code, the core of your library 59 | - `./pkg`: `wasm-pack` built npm-ready library 60 | - `./src`: TypeScript source code, where u can import from `wasm-pack` 61 | - `./example`: A web app that uses your library directly from `./dist` 62 | - `./tests`: To test your library 63 | 64 | ## License 65 | 66 | MIT © [Rust TypeScript Template](https://github.com/iantheearl/rust-typescript-template) 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Ian Huang ", 3 | "name": "rust-typescript-template", 4 | "version": "0.1.8", 5 | "module": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "prepare": "husky install", 14 | "predev": "wasm-pack build --out-name index --target web", 15 | "dev": "npm-run-all --parallel \"dev:rs\" \"dev:ts\"", 16 | "dev:ts": "npm run build -- -w", 17 | "dev:rs": "cargo watch -w core -s \"wasm-pack build --out-name index --target web\"", 18 | "test": "cargo test && wasm-pack test --headless", 19 | "build": "npm run clean && rollup -c", 20 | "lint": "eslint . --ext .ts", 21 | "example": "cd example && webpack-dev-server --open", 22 | "deploy": "cd example && webpack && cd .. && node publish.js" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.16.7", 26 | "@babel/preset-env": "^7.16.7", 27 | "@rollup/plugin-babel": "^5.3.0", 28 | "@rollup/plugin-node-resolve": "^13.1.2", 29 | "@rollup/plugin-typescript": "^8.3.0", 30 | "@typescript-eslint/eslint-plugin": "^5.9.0", 31 | "@typescript-eslint/parser": "^5.9.0", 32 | "copy-webpack-plugin": "^10.2.0", 33 | "eslint": "^8.6.0", 34 | "eslint-config-prettier": "^8.3.0", 35 | "eslint-plugin-import": "^2.25.4", 36 | "eslint-plugin-prettier": "^4.0.0", 37 | "eslint-plugin-unused-imports": "^2.0.0", 38 | "gh-pages": "^3.2.3", 39 | "husky": "^7.0.4", 40 | "lint-staged": "^12.1.5", 41 | "npm-run-all": "^4.1.5", 42 | "prettier": "^2.5.1", 43 | "pretty-quick": "^3.1.3", 44 | "rimraf": "^3.0.2", 45 | "rollup": "^2.63.0", 46 | "rollup-plugin-base64": "^1.0.1", 47 | "rollup-plugin-node-polyfills": "^0.2.1", 48 | "rollup-plugin-terser": "^7.0.2", 49 | "rollup-plugin-typescript2": "^0.31.1", 50 | "tslib": "^2.3.1", 51 | "typescript": "^4.5.4", 52 | "wasm-pack": "^0.12.1", 53 | "webpack": "^5.65.0", 54 | "webpack-cli": "^4.9.1", 55 | "webpack-dev-server": "^4.7.2" 56 | }, 57 | "lint-staged": { 58 | "{src,example}/**/*.{js,jsx,ts,tsx}": [ 59 | "eslint --fix", 60 | "pretty-quick --staged" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Universe } from "../pkg"; 2 | 3 | import { init } from "./wasm"; 4 | 5 | const CELL_SIZE = 5; // px 6 | const GRID_COLOR = "#CCCCCC"; 7 | const DEAD_COLOR = "#FFFFFF"; 8 | const ALIVE_COLOR = "#000000"; 9 | 10 | export class GameOfLife { 11 | universe!: Universe; 12 | memory!: WebAssembly.Memory; 13 | animationId = null as any; 14 | 15 | canvas!: HTMLCanvasElement; 16 | ctx!: CanvasRenderingContext2D; 17 | 18 | paused = true; 19 | 20 | public async initialize(): Promise { 21 | const { memory } = await init(); 22 | this.memory = memory; 23 | 24 | // Construct the universe, and get its width and height. 25 | this.universe = Universe.new(); 26 | this.createCanvas(); 27 | 28 | this.drawGrid(); 29 | this.drawCells(); 30 | } 31 | 32 | public resume = () => { 33 | this.animationId = requestAnimationFrame(this.renderLoop); 34 | this.paused = false; 35 | }; 36 | 37 | public pause = () => { 38 | cancelAnimationFrame(this.animationId); 39 | this.animationId = null; 40 | this.paused = true; 41 | }; 42 | 43 | private createCanvas = () => { 44 | this.canvas = document.getElementById("game-canvas") as HTMLCanvasElement; 45 | this.canvas.height = (CELL_SIZE + 1) * this.height + 1; 46 | this.canvas.width = (CELL_SIZE + 1) * this.width + 1; 47 | 48 | if (!this.canvas) { 49 | this.canvas = document.createElement("canvas"); 50 | this.canvas.id = "game-canvas"; 51 | document.body.appendChild(this.canvas); 52 | } 53 | 54 | this.canvas.addEventListener("click", (event) => { 55 | const boundingRect = this.canvas.getBoundingClientRect(); 56 | 57 | const scaleX = this.canvas.width / boundingRect.width; 58 | const scaleY = this.canvas.height / boundingRect.height; 59 | 60 | const canvasLeft = (event.clientX - boundingRect.left) * scaleX; 61 | const canvasTop = (event.clientY - boundingRect.top) * scaleY; 62 | 63 | const row = Math.min( 64 | Math.floor(canvasTop / (CELL_SIZE + 1)), 65 | this.height - 1 66 | ); 67 | const col = Math.min( 68 | Math.floor(canvasLeft / (CELL_SIZE + 1)), 69 | this.width - 1 70 | ); 71 | 72 | this.universe.toggleCell(row, col); 73 | 74 | this.drawCells(); 75 | }); 76 | 77 | this.ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D; 78 | }; 79 | 80 | private drawGrid = () => { 81 | this.ctx.beginPath(); 82 | this.ctx.strokeStyle = GRID_COLOR; 83 | 84 | // Vertical lines. 85 | for (let i = 0; i <= this.width; i++) { 86 | this.ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0); 87 | this.ctx.lineTo( 88 | i * (CELL_SIZE + 1) + 1, 89 | (CELL_SIZE + 1) * this.height + 1 90 | ); 91 | } 92 | 93 | // Horizontal lines. 94 | for (let j = 0; j <= this.height; j++) { 95 | this.ctx.moveTo(0, j * (CELL_SIZE + 1) + 1); 96 | this.ctx.lineTo( 97 | (CELL_SIZE + 1) * this.width + 1, 98 | j * (CELL_SIZE + 1) + 1 99 | ); 100 | } 101 | 102 | this.ctx.stroke(); 103 | }; 104 | 105 | private drawCells = () => { 106 | const cellsPtr = this.universe.cells(); 107 | const cells = new Uint8Array( 108 | this.memory.buffer, 109 | cellsPtr, 110 | (this.width * this.height) / 8 111 | ); 112 | 113 | this.ctx.beginPath(); 114 | 115 | // Alive cells. 116 | this.ctx.fillStyle = ALIVE_COLOR; 117 | for (let row = 0; row < this.height; row++) { 118 | for (let col = 0; col < this.width; col++) { 119 | const idx = this.getIndex(row, col); 120 | if (!this.bitIsSet(idx, cells)) { 121 | continue; 122 | } 123 | 124 | this.ctx.fillRect( 125 | col * (CELL_SIZE + 1) + 1, 126 | row * (CELL_SIZE + 1) + 1, 127 | CELL_SIZE, 128 | CELL_SIZE 129 | ); 130 | } 131 | } 132 | 133 | // Dead cells. 134 | this.ctx.fillStyle = DEAD_COLOR; 135 | for (let row = 0; row < this.height; row++) { 136 | for (let col = 0; col < this.width; col++) { 137 | const idx = this.getIndex(row, col); 138 | if (this.bitIsSet(idx, cells)) { 139 | continue; 140 | } 141 | 142 | this.ctx.fillRect( 143 | col * (CELL_SIZE + 1) + 1, 144 | row * (CELL_SIZE + 1) + 1, 145 | CELL_SIZE, 146 | CELL_SIZE 147 | ); 148 | } 149 | } 150 | 151 | this.ctx.stroke(); 152 | }; 153 | 154 | private renderLoop = () => { 155 | this.universe.tick(); 156 | this.drawCells(); 157 | this.animationId = requestAnimationFrame(this.renderLoop); 158 | }; 159 | 160 | private getIndex = (row: number, column: number) => { 161 | return row * this.width + column; 162 | }; 163 | 164 | private bitIsSet = (n: number, arr: Uint8Array) => { 165 | const byte = Math.floor(n / 8); 166 | const mask = 1 << n % 8; 167 | return (arr[byte] & mask) === mask; 168 | }; 169 | 170 | private get width() { 171 | return this.universe.width(); 172 | } 173 | 174 | private get height() { 175 | return this.universe.height(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /core/lib.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use fixedbitset::FixedBitSet; 4 | use std::fmt; 5 | use wasm_bindgen::prelude::*; 6 | 7 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 8 | // allocator. 9 | #[cfg(feature = "wee_alloc")] 10 | #[global_allocator] 11 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 12 | 13 | #[wasm_bindgen] 14 | pub struct Universe { 15 | width: u32, 16 | height: u32, 17 | cells: FixedBitSet, 18 | back_cells: FixedBitSet, 19 | } 20 | 21 | impl Universe { 22 | fn get_index(&self, row: u32, column: u32) -> usize { 23 | (row * self.width + column) as usize 24 | } 25 | 26 | fn live_neighbor_count(&self, row: u32, column: u32) -> u8 { 27 | let mut count = 0; 28 | let cells = &self.cells; 29 | 30 | let north = if row == 0 { self.height - 1 } else { row - 1 }; 31 | 32 | let south = if row == self.height - 1 { 0 } else { row + 1 }; 33 | 34 | let west = if column == 0 { 35 | self.width - 1 36 | } else { 37 | column - 1 38 | }; 39 | 40 | let east = if column == self.width - 1 { 41 | 0 42 | } else { 43 | column + 1 44 | }; 45 | 46 | let nw = self.get_index(north, west); 47 | count += cells[nw] as u8; 48 | 49 | let n = self.get_index(north, column); 50 | count += cells[n] as u8; 51 | 52 | let ne = self.get_index(north, east); 53 | count += cells[ne] as u8; 54 | 55 | let w = self.get_index(row, west); 56 | count += cells[w] as u8; 57 | 58 | let e = self.get_index(row, east); 59 | count += cells[e] as u8; 60 | 61 | let sw = self.get_index(south, west); 62 | count += cells[sw] as u8; 63 | 64 | let s = self.get_index(south, column); 65 | count += cells[s] as u8; 66 | 67 | let se = self.get_index(south, east); 68 | count += cells[se] as u8; 69 | 70 | count 71 | } 72 | 73 | /// Get the dead and alive values of the entire universe. 74 | pub fn get_cells(&self) -> *const u32 { 75 | self.cells.as_slice().as_ptr() 76 | } 77 | 78 | /// Set cells to be alive in a universe by passing the row and column 79 | /// of each cell as an array. 80 | pub fn set_cells(&mut self, cells: &[(u32, u32)]) { 81 | for (row, col) in cells.iter().cloned() { 82 | let idx = self.get_index(row, col); 83 | self.cells.set(idx, true); 84 | } 85 | } 86 | } 87 | 88 | #[wasm_bindgen] 89 | impl Universe { 90 | pub fn tick(&mut self) { 91 | for row in 0..self.height { 92 | for col in 0..self.width { 93 | let idx = self.get_index(row, col); 94 | let cell = self.cells[idx]; 95 | let live_neighbors = self.live_neighbor_count(row, col); 96 | 97 | let next_cell = match (cell, live_neighbors) { 98 | // Rule 1: Any live cell with fewer than two live neighbours 99 | // dies, as if caused by underpopulation. 100 | (true, x) if x < 2 => false, 101 | // Rule 2: Any live cell with two or three live neighbours 102 | // lives on to the next generation. 103 | (true, 2) | (true, 3) => true, 104 | // Rule 3: Any live cell with more than three live 105 | // neighbours dies, as if by overpopulation. 106 | (true, x) if x > 3 => false, 107 | // Rule 4: Any dead cell with exactly three live neighbours 108 | // becomes a live cell, as if by reproduction. 109 | (false, 3) => true, 110 | // All other cells remain in the same state. 111 | (otherwise, _) => otherwise, 112 | }; 113 | 114 | self.back_cells.set(idx, next_cell); 115 | } 116 | } 117 | 118 | std::mem::swap(&mut self.cells, &mut self.back_cells); 119 | } 120 | 121 | pub fn new() -> Universe { 122 | utils::set_panic_hook(); 123 | let width = 64; 124 | let height = 64; 125 | 126 | let size = (width * height) as usize; 127 | let mut cells = FixedBitSet::with_capacity(size); 128 | for i in 0..size { 129 | cells.set(i, i % 2 == 0 || i % 7 == 0); 130 | } 131 | let back_cells = cells.clone(); 132 | 133 | Universe { 134 | width, 135 | height, 136 | cells, 137 | back_cells, 138 | } 139 | } 140 | 141 | pub fn render(&self) -> String { 142 | self.to_string() 143 | } 144 | 145 | pub fn width(&self) -> u32 { 146 | self.width 147 | } 148 | 149 | pub fn height(&self) -> u32 { 150 | self.height 151 | } 152 | 153 | pub fn cells(&self) -> *const u32 { 154 | self.cells.as_slice().as_ptr() 155 | } 156 | 157 | /// Set the width of the universe. 158 | /// 159 | /// Resets all cells to the dead state. 160 | #[wasm_bindgen(js_name = setWidth)] 161 | pub fn set_width(&mut self, width: u32) { 162 | self.width = width; 163 | self.cells.clear(); 164 | } 165 | 166 | /// Set the height of the universe. 167 | /// 168 | /// Resets all cells to the dead state. 169 | #[wasm_bindgen(js_name = setHeight)] 170 | pub fn set_height(&mut self, height: u32) { 171 | self.height = height; 172 | self.cells.clear(); 173 | } 174 | 175 | #[wasm_bindgen(js_name = toggleCell)] 176 | pub fn toggle_cell(&mut self, row: u32, column: u32) { 177 | let idx = self.get_index(row, column); 178 | self.cells.set(idx, !self.cells[idx]); // toggle the value of the cell 179 | } 180 | } 181 | 182 | impl fmt::Display for Universe { 183 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 184 | let cells = &self.cells; 185 | for row in 0..self.height { 186 | for col in 0..self.width { 187 | let i = self.get_index(row, col); 188 | let symbol = if cells[i] { '◻' } else { '◼' }; 189 | write!(f, "{}", symbol)?; 190 | } 191 | write!(f, "\n")?; 192 | } 193 | 194 | Ok(()) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bumpalo" 7 | version = "3.8.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" 10 | 11 | [[package]] 12 | name = "cfg-if" 13 | version = "0.1.10" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "console_error_panic_hook" 25 | version = "0.1.7" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 28 | dependencies = [ 29 | "cfg-if 1.0.0", 30 | "wasm-bindgen", 31 | ] 32 | 33 | [[package]] 34 | name = "fixedbitset" 35 | version = "0.4.1" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" 38 | 39 | [[package]] 40 | name = "futures" 41 | version = "0.3.19" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" 44 | dependencies = [ 45 | "futures-channel", 46 | "futures-core", 47 | "futures-executor", 48 | "futures-io", 49 | "futures-sink", 50 | "futures-task", 51 | "futures-util", 52 | ] 53 | 54 | [[package]] 55 | name = "futures-channel" 56 | version = "0.3.19" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" 59 | dependencies = [ 60 | "futures-core", 61 | "futures-sink", 62 | ] 63 | 64 | [[package]] 65 | name = "futures-core" 66 | version = "0.3.19" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" 69 | 70 | [[package]] 71 | name = "futures-executor" 72 | version = "0.3.19" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" 75 | dependencies = [ 76 | "futures-core", 77 | "futures-task", 78 | "futures-util", 79 | ] 80 | 81 | [[package]] 82 | name = "futures-io" 83 | version = "0.3.19" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" 86 | 87 | [[package]] 88 | name = "futures-macro" 89 | version = "0.3.19" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" 92 | dependencies = [ 93 | "proc-macro2", 94 | "quote", 95 | "syn", 96 | ] 97 | 98 | [[package]] 99 | name = "futures-sink" 100 | version = "0.3.19" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" 103 | 104 | [[package]] 105 | name = "futures-task" 106 | version = "0.3.19" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" 109 | 110 | [[package]] 111 | name = "futures-util" 112 | version = "0.3.19" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" 115 | dependencies = [ 116 | "futures-channel", 117 | "futures-core", 118 | "futures-io", 119 | "futures-macro", 120 | "futures-sink", 121 | "futures-task", 122 | "memchr", 123 | "pin-project-lite", 124 | "pin-utils", 125 | "slab", 126 | ] 127 | 128 | [[package]] 129 | name = "js-sys" 130 | version = "0.3.55" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" 133 | dependencies = [ 134 | "wasm-bindgen", 135 | ] 136 | 137 | [[package]] 138 | name = "lazy_static" 139 | version = "1.4.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 142 | 143 | [[package]] 144 | name = "libc" 145 | version = "0.2.112" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" 148 | 149 | [[package]] 150 | name = "log" 151 | version = "0.4.14" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 154 | dependencies = [ 155 | "cfg-if 1.0.0", 156 | ] 157 | 158 | [[package]] 159 | name = "memchr" 160 | version = "2.4.1" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 163 | 164 | [[package]] 165 | name = "memory_units" 166 | version = "0.4.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" 169 | 170 | [[package]] 171 | name = "pin-project-lite" 172 | version = "0.2.8" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" 175 | 176 | [[package]] 177 | name = "pin-utils" 178 | version = "0.1.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 181 | 182 | [[package]] 183 | name = "proc-macro2" 184 | version = "1.0.36" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 187 | dependencies = [ 188 | "unicode-xid", 189 | ] 190 | 191 | [[package]] 192 | name = "quote" 193 | version = "1.0.14" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" 196 | dependencies = [ 197 | "proc-macro2", 198 | ] 199 | 200 | [[package]] 201 | name = "rust-typescript-template" 202 | version = "0.1.0" 203 | dependencies = [ 204 | "fixedbitset", 205 | "futures", 206 | "js-sys", 207 | "wasm-bindgen", 208 | "wasm-bindgen-futures", 209 | "wasm-bindgen-test", 210 | "web-sys", 211 | "wee_alloc", 212 | ] 213 | 214 | [[package]] 215 | name = "scoped-tls" 216 | version = "1.0.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" 219 | 220 | [[package]] 221 | name = "slab" 222 | version = "0.4.5" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" 225 | 226 | [[package]] 227 | name = "syn" 228 | version = "1.0.84" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" 231 | dependencies = [ 232 | "proc-macro2", 233 | "quote", 234 | "unicode-xid", 235 | ] 236 | 237 | [[package]] 238 | name = "unicode-xid" 239 | version = "0.2.2" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 242 | 243 | [[package]] 244 | name = "wasm-bindgen" 245 | version = "0.2.78" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" 248 | dependencies = [ 249 | "cfg-if 1.0.0", 250 | "wasm-bindgen-macro", 251 | ] 252 | 253 | [[package]] 254 | name = "wasm-bindgen-backend" 255 | version = "0.2.78" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" 258 | dependencies = [ 259 | "bumpalo", 260 | "lazy_static", 261 | "log", 262 | "proc-macro2", 263 | "quote", 264 | "syn", 265 | "wasm-bindgen-shared", 266 | ] 267 | 268 | [[package]] 269 | name = "wasm-bindgen-futures" 270 | version = "0.4.28" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" 273 | dependencies = [ 274 | "cfg-if 1.0.0", 275 | "js-sys", 276 | "wasm-bindgen", 277 | "web-sys", 278 | ] 279 | 280 | [[package]] 281 | name = "wasm-bindgen-macro" 282 | version = "0.2.78" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" 285 | dependencies = [ 286 | "quote", 287 | "wasm-bindgen-macro-support", 288 | ] 289 | 290 | [[package]] 291 | name = "wasm-bindgen-macro-support" 292 | version = "0.2.78" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" 295 | dependencies = [ 296 | "proc-macro2", 297 | "quote", 298 | "syn", 299 | "wasm-bindgen-backend", 300 | "wasm-bindgen-shared", 301 | ] 302 | 303 | [[package]] 304 | name = "wasm-bindgen-shared" 305 | version = "0.2.78" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" 308 | 309 | [[package]] 310 | name = "wasm-bindgen-test" 311 | version = "0.3.28" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "96f1aa7971fdf61ef0f353602102dbea75a56e225ed036c1e3740564b91e6b7e" 314 | dependencies = [ 315 | "console_error_panic_hook", 316 | "js-sys", 317 | "scoped-tls", 318 | "wasm-bindgen", 319 | "wasm-bindgen-futures", 320 | "wasm-bindgen-test-macro", 321 | ] 322 | 323 | [[package]] 324 | name = "wasm-bindgen-test-macro" 325 | version = "0.3.28" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "6006f79628dfeb96a86d4db51fbf1344cd7fd8408f06fc9aa3c84913a4789688" 328 | dependencies = [ 329 | "proc-macro2", 330 | "quote", 331 | ] 332 | 333 | [[package]] 334 | name = "web-sys" 335 | version = "0.3.55" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" 338 | dependencies = [ 339 | "js-sys", 340 | "wasm-bindgen", 341 | ] 342 | 343 | [[package]] 344 | name = "wee_alloc" 345 | version = "0.4.5" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" 348 | dependencies = [ 349 | "cfg-if 0.1.10", 350 | "libc", 351 | "memory_units", 352 | "winapi", 353 | ] 354 | 355 | [[package]] 356 | name = "winapi" 357 | version = "0.3.9" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 360 | dependencies = [ 361 | "winapi-i686-pc-windows-gnu", 362 | "winapi-x86_64-pc-windows-gnu", 363 | ] 364 | 365 | [[package]] 366 | name = "winapi-i686-pc-windows-gnu" 367 | version = "0.4.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 370 | 371 | [[package]] 372 | name = "winapi-x86_64-pc-windows-gnu" 373 | version = "0.4.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 376 | --------------------------------------------------------------------------------