├── .gitignore ├── assets ├── banner.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png └── styles.css ├── src ├── components │ ├── mod.rs │ ├── pattern_selector.rs │ ├── board.rs │ └── game.rs ├── lexicon │ ├── lexicon.bin │ └── mod.rs ├── settings.rs ├── main.rs ├── color_utils.rs └── life │ └── mod.rs ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── main.yml ├── README.md ├── index.html └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /dist 3 | .DS_Store -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scastiel/lifeee-rs/HEAD/assets/banner.png -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod board; 2 | pub mod game; 3 | pub mod pattern_selector; 4 | -------------------------------------------------------------------------------- /assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scastiel/lifeee-rs/HEAD/assets/favicon-16x16.png -------------------------------------------------------------------------------- /assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scastiel/lifeee-rs/HEAD/assets/favicon-32x32.png -------------------------------------------------------------------------------- /assets/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scastiel/lifeee-rs/HEAD/assets/favicon-96x96.png -------------------------------------------------------------------------------- /src/lexicon/lexicon.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scastiel/lifeee-rs/HEAD/src/lexicon/lexicon.bin -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, PartialEq)] 2 | pub struct Settings { 3 | pub cell_size: f64, 4 | pub grid_width: f64, 5 | pub num_previous: usize, 6 | } 7 | 8 | pub fn default_settings() -> Settings { 9 | Settings { 10 | cell_size: 20.0, 11 | grid_width: 0.5, 12 | num_previous: 10, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | mod color_utils; 4 | mod components; 5 | mod life; 6 | mod settings; 7 | 8 | use components::game::Game; 9 | use settings::{default_settings, Settings}; 10 | 11 | #[function_component(App)] 12 | fn app() -> Html { 13 | html! { 14 | context={default_settings()}> 15 | 16 | > 17 | } 18 | } 19 | 20 | fn main() { 21 | yew::start_app::(); 22 | } 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yew-app" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew = { git = "https://github.com/yewstack/yew/" } 10 | gloo = "0.3" 11 | wasm-bindgen = "0.2.74" 12 | serde = { version = "1.0", features = ["derive"] } 13 | bincode = "1.2.1" 14 | lexicon = "0.1.2" 15 | 16 | [dependencies.web-sys] 17 | version = "0.3.4" 18 | features = [ 19 | 'CanvasRenderingContext2d', 20 | 'Document', 21 | 'Element', 22 | 'HtmlCanvasElement', 23 | 'HtmlSelectElement', 24 | 'Window', 25 | 'WheelEvent', 26 | ] 27 | -------------------------------------------------------------------------------- /src/color_utils.rs: -------------------------------------------------------------------------------- 1 | pub fn grey(coeff: f64) -> String { 2 | let coeff = f64::min(f64::max(coeff, 0.0), 1.0); 3 | let v = (coeff * 255.0) as u8; 4 | format!("#{:0>2x}{:0>2x}{:0>2x}", v, v, v) 5 | } 6 | 7 | #[cfg(test)] 8 | mod tests { 9 | use super::*; 10 | 11 | #[test] 12 | fn grey_minus_0_5_returns_black() { 13 | assert_eq!(grey(-0.5), "#000000".to_string()) 14 | } 15 | 16 | #[test] 17 | fn grey_0_returns_black() { 18 | assert_eq!(grey(0.0), "#000000".to_string()) 19 | } 20 | 21 | #[test] 22 | fn grey_0_5_returns_some_grey() { 23 | assert_eq!(grey(0.5), "#7f7f7f".to_string()) 24 | } 25 | 26 | #[test] 27 | fn grey_1_returns_white() { 28 | assert_eq!(grey(1.0), "#ffffff".to_string()) 29 | } 30 | 31 | #[test] 32 | fn grey_1_5_returns_white() { 33 | assert_eq!(grey(1.5), "#ffffff".to_string()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lexicon/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fmt::Debug; 3 | 4 | #[derive(Debug)] 5 | pub struct ParseError(String); 6 | 7 | #[derive(Hash, Eq, PartialEq, Debug, Clone, Copy, Serialize, Deserialize)] 8 | pub struct Cell { 9 | pub x: i32, 10 | pub y: i32, 11 | } 12 | 13 | #[derive(Debug, Clone, Serialize, Deserialize)] 14 | pub struct Term { 15 | pub name: String, 16 | pub description: String, 17 | pub tags: Vec, 18 | pub cells: Vec, 19 | pub width: usize, 20 | pub height: usize, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize)] 24 | pub struct Lexicon { 25 | pub terms: Vec, 26 | } 27 | 28 | impl Lexicon { 29 | pub fn get_term(&self, name: String) -> Option<&Term> { 30 | self.terms.iter().find(|term| term.name == name) 31 | } 32 | } 33 | 34 | pub fn get_lexicon() -> Option { 35 | let serialized = include_bytes!("lexicon.bin"); 36 | let lexicon: Lexicon = bincode::deserialize(serialized).unwrap(); 37 | Some(lexicon) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2021 Sebastien Castiel 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - uses: actions/cache@v2 18 | with: 19 | path: | 20 | ~/.cargo/bin/ 21 | ~/.cargo/registry/index/ 22 | ~/.cargo/registry/cache/ 23 | ~/.cargo/git/db/ 24 | target/ 25 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 26 | 27 | - name: Install Rust Toolchain 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | toolchain: stable 31 | override: true 32 | target: wasm32-unknown-unknown 33 | 34 | - name: Install Trunk 35 | uses: jetli/trunk-action@v0.1.0 36 | 37 | - name: Build 38 | run: trunk build --release 39 | 40 | - name: Publish 41 | uses: netlify/actions/cli@master 42 | with: 43 | args: deploy --dir=dist --prod 44 | env: 45 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 46 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Lifeee](https://lifeee.netlify.app) – An implementation of the Game of Life 2 | 3 | I realized this application to keep learning [Rust](https://www.rust-lang.org/), discover the front-end library [Yew](https://yew.rs/), and because I’m a big fan of [John Conway’s Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). Please consider it always a work-in-progress. 4 | 5 | ## Features 6 | 7 | - Draggable & zoomable **infinite grid** 8 | - Adjustable **speed** of simulation 9 | - Library of **patterns** extracted from the official [Lexicon](https://playgameoflife.com/lexicon) 10 | 11 | ## Work-in-progress features 12 | 13 | - Better support for mobile (pinch-and-zoom) 14 | - Sexier view of the pattern library (descriptions, search, etc.) 15 | - Make the view _follow_ the displayed pattern 16 | - Draw your own pattern on the grid 17 | - Compose several patterns in a simulation 18 | - Import & export RLE files 19 | 20 | ## Run locally 21 | 22 | To start the application locally, run the following commands: 23 | 24 | Use `nightly`: 25 | 26 | ``` 27 | rustup default nightly && rustup update 28 | ``` 29 | 30 | Install `trunk` and `wasm-bingen-cli`: 31 | 32 | ``` 33 | cargo install trunk wasm-bindgen-cli 34 | ``` 35 | 36 | Add `wasm32-unknown-unknown` target: 37 | 38 | ``` 39 | rustup target add wasm32-unknown-unknown 40 | ``` 41 | 42 | Run the application using `trunk`: 43 | 44 | ``` 45 | trunk serve 46 | ``` 47 | 48 | ## Want to contribute? 49 | 50 | Please do 😉 51 | 52 | ## License 53 | 54 | See [LICENSE](https://github.com/scastiel/lifeee-rs/blob/main/LICENSE). 55 | -------------------------------------------------------------------------------- /src/life/mod.rs: -------------------------------------------------------------------------------- 1 | use lexicon::Cell; 2 | use std::collections::HashSet; 3 | 4 | pub type CellSet = HashSet; 5 | 6 | fn singleton(cell: Cell) -> CellSet { 7 | let mut cells = CellSet::new(); 8 | cells.insert(cell); 9 | cells 10 | } 11 | 12 | pub fn make_cell_alive(cells: &CellSet, cell: Cell) -> CellSet { 13 | cells.union(&singleton(cell)).copied().collect() 14 | } 15 | 16 | pub fn make_cell_dead(cells: &CellSet, cell: Cell) -> CellSet { 17 | cells.difference(&singleton(cell)).copied().collect() 18 | } 19 | 20 | pub fn tick(cells: &CellSet) -> CellSet { 21 | cells_with_neighbors(cells) 22 | .iter() 23 | .filter(|&&cell| { 24 | let alive_neighbors = number_of_alive_neighbors(cells, cell); 25 | if cell_is_alive(cells, cell) { 26 | alive_neighbors == 2 || alive_neighbors == 3 27 | } else { 28 | alive_neighbors == 3 29 | } 30 | }) 31 | .map(|&c| c) 32 | .collect() 33 | } 34 | 35 | pub fn cell_is_alive(cells: &CellSet, cell: Cell) -> bool { 36 | cells.contains(&cell) 37 | } 38 | 39 | fn cells_with_neighbors(cells: &HashSet) -> CellSet { 40 | cells 41 | .iter() 42 | .flat_map(|&cell| { 43 | let mut neighbors = cell_neighbors(cell); 44 | neighbors.push(cell); 45 | neighbors 46 | }) 47 | .collect() 48 | } 49 | 50 | fn number_of_alive_neighbors(cells: &CellSet, cell: Cell) -> usize { 51 | cell_neighbors(cell) 52 | .iter() 53 | .map(|&neighbor| cell_is_alive(cells, neighbor)) 54 | .filter(|&b| b) 55 | .count() 56 | } 57 | 58 | fn cell_neighbors(cell: Cell) -> Vec { 59 | (-1..=1) 60 | .into_iter() 61 | .flat_map(|dx| { 62 | (-1..=1).into_iter().flat_map(move |dy| { 63 | let mut set = vec![]; 64 | if dx != 0 || dy != 0 { 65 | set.push(Cell { 66 | x: cell.x + dx, 67 | y: cell.y + dy, 68 | }); 69 | } 70 | set 71 | }) 72 | }) 73 | .collect() 74 | } 75 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Lifeee – Play John Conway’s Game of Life 8 | 9 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 34 | 38 | 42 | 43 | 50 | 57 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/components/pattern_selector.rs: -------------------------------------------------------------------------------- 1 | use lexicon::*; 2 | use wasm_bindgen::JsCast; 3 | use web_sys::HtmlSelectElement; 4 | use yew::prelude::*; 5 | 6 | pub struct PatternSelector { 7 | lexicon: Lexicon, 8 | selected: Option, 9 | } 10 | 11 | #[derive(Properties, PartialEq)] 12 | pub struct Props { 13 | pub on_apply_pattern: Callback, 14 | } 15 | 16 | pub enum Msg { 17 | PatternChanged(usize), 18 | Apply, 19 | } 20 | 21 | impl Component for PatternSelector { 22 | type Message = Msg; 23 | type Properties = Props; 24 | 25 | fn create(_: &Context) -> Self { 26 | Self { 27 | lexicon: Lexicon::get(), 28 | selected: None, 29 | } 30 | } 31 | 32 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 33 | match msg { 34 | Msg::PatternChanged(selected) => { 35 | self.selected = Some(selected); 36 | true 37 | } 38 | Msg::Apply => { 39 | if let Some(selected) = self.selected { 40 | let on_apply_pattern = ctx.props().on_apply_pattern.clone(); 41 | let selected_term = self.lexicon.terms[selected].clone(); 42 | on_apply_pattern.emit(selected_term); 43 | } 44 | true 45 | } 46 | } 47 | } 48 | 49 | fn view(&self, ctx: &Context) -> yew::virtual_dom::VNode { 50 | let on_change_selected = ctx.link().callback(|event: Event| { 51 | let input = event 52 | .target() 53 | .and_then(|t| t.dyn_into::().ok()) 54 | .unwrap(); 55 | let selected: usize = input.value().parse().unwrap(); 56 | Msg::PatternChanged(selected) 57 | }); 58 | 59 | html! { 60 | 61 | 62 | {"Select a pattern…"} 63 | {for { 64 | self.lexicon.terms.iter().enumerate() 65 | .filter(|(_, term)| !term.cells.is_empty()) 66 | .map(|(i, term)| html! { 67 | {format_term_option(&term)} 71 | }) 72 | }} 73 | 74 | {"Apply"} 75 | 76 | } 77 | } 78 | } 79 | 80 | fn format_term_option(term: &Term) -> String { 81 | format!( 82 | "{}{}", 83 | &term.name, 84 | if (&term.tags).len() > 0 { 85 | format!(" ({})", &term.tags.join(", ")) 86 | } else { 87 | "".to_string() 88 | } 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #0d008b; 3 | } 4 | body { 5 | overflow: hidden; 6 | font-family: sans-serif; 7 | } 8 | .board { 9 | cursor: move; 10 | touch-action: none; 11 | position: absolute; 12 | top: 0; 13 | right: 0; 14 | bottom: 0; 15 | left: 0; 16 | } 17 | .panel { 18 | font-size: 14px; 19 | background: white; 20 | position: absolute; 21 | bottom: 10px; 22 | left: 10px; 23 | box-shadow: 0 5px 10px #00000020; 24 | border-radius: 5px; 25 | padding: 10px; 26 | } 27 | button { 28 | background-color: var(--primary-color); 29 | border: 0; 30 | color: white; 31 | padding: 4px 8px; 32 | border-radius: 4px; 33 | min-width: 60px; 34 | min-height: 25px; 35 | cursor: pointer; 36 | } 37 | select { 38 | padding: 3px 8px; 39 | border: 1px solid var(--primary-color); 40 | border-radius: 4px; 41 | } 42 | label { 43 | display: flex; 44 | align-items: center; 45 | } 46 | label > *:first-child { 47 | color: var(--primary-color); 48 | width: 100px; 49 | } 50 | .controls { 51 | margin-bottom: 8px; 52 | padding-bottom: 8px; 53 | border-bottom: 1px solid #eee; 54 | display: flex; 55 | } 56 | .controls button { 57 | margin-right: 8px; 58 | min-width: 60px; 59 | } 60 | .generation { 61 | flex: 1; 62 | text-align: right; 63 | font-size: small; 64 | color: darkgray; 65 | } 66 | .pattern-selector { 67 | margin-bottom: 8px; 68 | } 69 | .pattern-selector select { 70 | width: 200px; 71 | } 72 | .pattern-selector button { 73 | margin-left: 4px; 74 | } 75 | .about { 76 | font-size: small; 77 | color: #222; 78 | margin-top: 8px; 79 | padding-top: 8px; 80 | border-top: 1px solid #eee; 81 | text-align: center; 82 | } 83 | .about a { 84 | color: var(--primary-color); 85 | } 86 | 87 | input[type='range'] { 88 | height: 25px; 89 | -webkit-appearance: none; 90 | margin: 0 0; 91 | width: 100%; 92 | } 93 | input[type='range']:focus { 94 | outline: none; 95 | } 96 | input[type='range']::-webkit-slider-runnable-track { 97 | width: 100%; 98 | height: 5px; 99 | cursor: pointer; 100 | animate: 0.2s; 101 | box-shadow: 0px 0px 0px #000000; 102 | background: rgb(30, 64, 175); 103 | border-radius: 1px; 104 | border: 0px solid #000000; 105 | } 106 | input[type='range']::-webkit-slider-thumb { 107 | box-shadow: 0px 0px 0px #000000; 108 | border: 1px solid rgb(30, 64, 175); 109 | height: 18px; 110 | width: 18px; 111 | border-radius: 25px; 112 | background: rgb(30, 64, 175); 113 | opacity: 0.8; 114 | cursor: pointer; 115 | -webkit-appearance: none; 116 | margin-top: -7px; 117 | } 118 | input[type='range']:focus::-webkit-slider-runnable-track { 119 | background: rgb(30, 64, 175); 120 | } 121 | input[type='range']::-moz-range-track { 122 | width: 100%; 123 | height: 5px; 124 | cursor: pointer; 125 | animate: 0.2s; 126 | box-shadow: 0px 0px 0px #000000; 127 | background: rgb(30, 64, 175); 128 | border-radius: 1px; 129 | border: 0px solid #000000; 130 | } 131 | input[type='range']::-moz-range-thumb { 132 | box-shadow: 0px 0px 0px #000000; 133 | border: 1px solid rgb(30, 64, 175); 134 | height: 18px; 135 | width: 18px; 136 | border-radius: 25px; 137 | background: rgb(30, 64, 175); 138 | opacity: 0.8; 139 | cursor: pointer; 140 | } 141 | input[type='range']::-ms-track { 142 | width: 100%; 143 | height: 5px; 144 | cursor: pointer; 145 | animate: 0.2s; 146 | background: transparent; 147 | border-color: transparent; 148 | color: transparent; 149 | } 150 | input[type='range']::-ms-fill-lower { 151 | background: rgb(30, 64, 175); 152 | border: 0px solid #000000; 153 | border-radius: 2px; 154 | box-shadow: 0px 0px 0px #000000; 155 | } 156 | input[type='range']::-ms-fill-upper { 157 | background: rgb(30, 64, 175); 158 | border: 0px solid #000000; 159 | border-radius: 2px; 160 | box-shadow: 0px 0px 0px #000000; 161 | } 162 | input[type='range']::-ms-thumb { 163 | margin-top: 1px; 164 | box-shadow: 0px 0px 0px #000000; 165 | border: 1px solid rgb(30, 64, 175); 166 | height: 18px; 167 | width: 18px; 168 | border-radius: 25px; 169 | background: rgb(30, 64, 175); 170 | opacity: 0.8; 171 | cursor: pointer; 172 | } 173 | input[type='range']:focus::-ms-fill-lower { 174 | background: rgb(30, 64, 175); 175 | } 176 | input[type='range']:focus::-ms-fill-upper { 177 | background: rgb(30, 64, 175); 178 | } 179 | -------------------------------------------------------------------------------- /src/components/board.rs: -------------------------------------------------------------------------------- 1 | use crate::color_utils::grey; 2 | use crate::life; 3 | use crate::settings::Settings; 4 | use lexicon::*; 5 | use wasm_bindgen::*; 6 | use web_sys::WheelEvent; 7 | use yew::prelude::*; 8 | 9 | #[derive(PartialEq, Properties)] 10 | pub struct BoardProps { 11 | pub cells: life::CellSet, 12 | pub previous_gens: Vec, 13 | pub offset: (f64, f64), 14 | pub zoom: f64, 15 | pub move_offset: Callback<(f64, f64)>, 16 | pub change_zoom: Callback<(i32, i32, f64)>, 17 | pub width: u32, 18 | pub height: u32, 19 | } 20 | 21 | pub struct Board { 22 | canvas_ref: NodeRef, 23 | last_offset: Option<(f64, f64)>, 24 | } 25 | 26 | impl Board { 27 | fn canvas(&self) -> web_sys::HtmlCanvasElement { 28 | self 29 | .canvas_ref 30 | .cast::() 31 | .unwrap() 32 | } 33 | 34 | fn context(&self) -> web_sys::CanvasRenderingContext2d { 35 | self 36 | .canvas() 37 | .get_context("2d") 38 | .unwrap() 39 | .unwrap() 40 | .dyn_into::() 41 | .unwrap() 42 | } 43 | 44 | fn cell_range( 45 | &self, 46 | settings: &Settings, 47 | offset: (f64, f64), 48 | zoom: f64, 49 | ) -> (std::ops::Range, std::ops::Range) { 50 | let canvas = self.canvas(); 51 | 52 | let from_x = self.size_to_cells(settings, -offset.0, zoom) as i32 - 1; 53 | let to_x = from_x + self.size_to_cells(settings, canvas.width() as f64, zoom) as i32 + 1; 54 | let from_y = self.size_to_cells(settings, -offset.1, zoom) as i32 - 1; 55 | let to_y = from_y + self.size_to_cells(settings, canvas.height() as f64, zoom) as i32 + 1; 56 | 57 | (from_x..to_x, from_y..to_y) 58 | } 59 | 60 | fn erase(&self) { 61 | let canvas = self.canvas(); 62 | let context = self.context(); 63 | context.set_fill_style(&JsValue::from_str("white")); 64 | context.fill_rect(0.0, 0.0, canvas.width().into(), canvas.height().into()) 65 | } 66 | 67 | fn size_to_cells(&self, settings: &Settings, size: f64, zoom: f64) -> f64 { 68 | (size / (settings.cell_size * zoom + settings.grid_width) as f64).ceil() 69 | } 70 | 71 | fn draw_grid(&self, settings: &Settings, offset: (f64, f64), zoom: f64) { 72 | let canvas = self.canvas(); 73 | let context = self.context(); 74 | context.set_fill_style(&JsValue::from_str(grey(0.9).as_str())); 75 | 76 | let (cell_range_x, cell_range_y) = self.cell_range(settings, offset, zoom); 77 | for i in cell_range_x { 78 | context.fill_rect( 79 | offset.0 + i as f64 * (zoom * settings.cell_size + settings.grid_width), 80 | 0.0, 81 | settings.grid_width, 82 | canvas.height().into(), 83 | ) 84 | } 85 | 86 | for j in cell_range_y { 87 | context.fill_rect( 88 | 0.0, 89 | offset.1 + (j as f64 * (zoom * settings.cell_size + settings.grid_width)), 90 | canvas.width().into(), 91 | settings.grid_width, 92 | ) 93 | } 94 | } 95 | 96 | fn draw_cells( 97 | &self, 98 | settings: &Settings, 99 | cells: &life::CellSet, 100 | color: String, 101 | offset: (f64, f64), 102 | zoom: f64, 103 | ) { 104 | let context = self.context(); 105 | context.set_fill_style(&JsValue::from(color)); 106 | 107 | let (cell_range_x, cell_range_y) = self.cell_range(settings, offset, zoom); 108 | let cells = cells.iter().filter(|Cell { x, y }| { 109 | *x >= cell_range_x.start 110 | && *x <= cell_range_x.end 111 | && *y >= cell_range_y.start 112 | && *y <= cell_range_y.end 113 | }); 114 | 115 | for cell in cells { 116 | context.fill_rect( 117 | offset.0 118 | + (settings.grid_width 119 | + (zoom * settings.cell_size + settings.grid_width) * cell.x as f64), 120 | offset.1 121 | + (settings.grid_width 122 | + (zoom * settings.cell_size + settings.grid_width) * cell.y as f64), 123 | zoom * settings.cell_size, 124 | zoom * settings.cell_size, 125 | ); 126 | } 127 | } 128 | 129 | fn color_for_previous_gen(&self, gen_index: usize, num_gens: usize) -> String { 130 | let from = 0.80_f64; 131 | let to = 0.99_f64; 132 | let coeff = gen_index as f64 * (to - from) / (num_gens as f64) + from; 133 | crate::color_utils::grey(coeff) 134 | } 135 | 136 | fn settings(&self, ctx: &Context) -> Settings { 137 | ctx 138 | .link() 139 | .context::(Callback::noop()) 140 | .expect("settings context to be set") 141 | .0 142 | } 143 | } 144 | 145 | pub enum BoardMessage { 146 | PointerDown(i32, i32), 147 | PointerUp(i32, i32), 148 | PointerMove(i32, i32), 149 | Zoom(i32, i32, f64), 150 | } 151 | 152 | impl Component for Board { 153 | type Message = BoardMessage; 154 | type Properties = BoardProps; 155 | 156 | fn create(_ctx: &Context) -> Self { 157 | Self { 158 | canvas_ref: NodeRef::default(), 159 | last_offset: None, 160 | } 161 | } 162 | 163 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 164 | match msg { 165 | BoardMessage::PointerDown(x, y) => { 166 | self.last_offset = Some((x as f64, y as f64)); 167 | false 168 | } 169 | BoardMessage::PointerUp(_x, _y) => { 170 | self.last_offset = None; 171 | false 172 | } 173 | BoardMessage::PointerMove(x, y) => { 174 | if let Some(last_offset) = self.last_offset { 175 | let offset = ctx.props().offset; 176 | let new_offset = ( 177 | offset.0 + x as f64 - last_offset.0, 178 | offset.1 + y as f64 - last_offset.1, 179 | ); 180 | if offset != new_offset { 181 | ctx.props().move_offset.emit(( 182 | offset.0 + x as f64 - last_offset.0, 183 | offset.1 + y as f64 - last_offset.1, 184 | )); 185 | } 186 | self.last_offset = Some((x as f64, y as f64)); 187 | true 188 | } else { 189 | false 190 | } 191 | } 192 | BoardMessage::Zoom(x1, y1, zoom) => { 193 | let zoom = f64::max(f64::min(ctx.props().zoom - 0.1 * zoom / 120.0, 5.0), 0.1); 194 | ctx.props().change_zoom.emit((x1, y1, zoom)); 195 | true 196 | } 197 | } 198 | } 199 | 200 | fn rendered(&mut self, ctx: &Context, _first_render: bool) { 201 | let canvas = self.canvas(); 202 | canvas.set_width(ctx.props().width); 203 | canvas.set_height(ctx.props().height); 204 | let settings = self.settings(ctx); 205 | let zoom = ctx.props().zoom; 206 | let offset = ctx.props().offset; 207 | self.erase(); 208 | if ctx.props().zoom > 0.3 { 209 | self.draw_grid(&settings, offset, zoom); 210 | } 211 | let previous_gens = &ctx.props().previous_gens; 212 | let num_gens = previous_gens.len(); 213 | for i in 0..num_gens { 214 | let gen_index = num_gens - i - 1; 215 | self.draw_cells( 216 | &settings, 217 | &previous_gens[gen_index], 218 | self.color_for_previous_gen(gen_index, num_gens), 219 | offset, 220 | zoom, 221 | ); 222 | } 223 | self.draw_cells( 224 | &settings, 225 | &ctx.props().cells, 226 | "#0d008b".to_string(), 227 | offset, 228 | zoom, 229 | ); 230 | } 231 | 232 | fn view(&self, ctx: &Context) -> Html { 233 | html! { 234 | 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/components/game.rs: -------------------------------------------------------------------------------- 1 | use crate::components::board::Board; 2 | use crate::components::pattern_selector::PatternSelector; 3 | use crate::life::*; 4 | use crate::Settings; 5 | use gloo::events::EventListener; 6 | use gloo::timers::callback::Interval; 7 | use lexicon::Term; 8 | use std::collections::VecDeque; 9 | use wasm_bindgen::JsCast; 10 | use web_sys::HtmlInputElement; 11 | use yew::prelude::*; 12 | 13 | pub struct Game { 14 | cells: CellSet, 15 | previous_gens: Vec, 16 | tick: u32, 17 | interval: Option, 18 | speed: u8, 19 | adjust_offset: Option<(usize, usize)>, 20 | offset: (f64, f64), 21 | zoom: f64, 22 | width: u32, 23 | height: u32, 24 | _resize_handle: EventListener, 25 | } 26 | 27 | pub enum Msg { 28 | NextTick, 29 | Play, 30 | Pause, 31 | ChangeSpeed(u8), 32 | ApplyPattern(Term), 33 | MoveOffset((f64, f64)), 34 | ChangeZoom((i32, i32, f64)), 35 | Resize, 36 | } 37 | 38 | impl Game { 39 | fn settings(&self, ctx: &Context) -> Settings { 40 | ctx 41 | .link() 42 | .context::(Callback::noop()) 43 | .expect("settings context to be set") 44 | .0 45 | } 46 | 47 | fn start_interval(&mut self, ctx: &Context) { 48 | let link = ctx.link().clone(); 49 | link.send_message(Msg::NextTick); 50 | let millis = (50_f64 - 500_f64) / 9_f64 * self.speed as f64 + 500_f64; 51 | let interval = Interval::new(millis as u32, move || link.send_message(Msg::NextTick)); 52 | self.interval = Some(interval); 53 | } 54 | } 55 | 56 | impl Component for Game { 57 | type Message = Msg; 58 | type Properties = (); 59 | 60 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 61 | let settings = self.settings(ctx); 62 | match msg { 63 | Msg::NextTick => { 64 | self.tick += 1; 65 | self.adjust_offset = None; 66 | 67 | self.previous_gens = { 68 | let mut previous_gens_deque: VecDeque = self 69 | .previous_gens 70 | .iter() 71 | .map(|cell_set| cell_set.clone()) 72 | .collect(); 73 | previous_gens_deque.push_front(self.cells.clone()); 74 | if previous_gens_deque.len() > settings.num_previous { 75 | previous_gens_deque.pop_back(); 76 | } 77 | previous_gens_deque 78 | .iter() 79 | .map(|cell_set| cell_set.clone()) 80 | .collect() 81 | }; 82 | 83 | self.cells = tick(&self.cells); 84 | 85 | true 86 | } 87 | Msg::Play => { 88 | self.start_interval(ctx); 89 | true 90 | } 91 | Msg::Pause => { 92 | self.interval = None; 93 | true 94 | } 95 | Msg::ChangeSpeed(speed) => { 96 | self.speed = speed; 97 | if self.interval.is_some() { 98 | self.start_interval(ctx); 99 | } 100 | true 101 | } 102 | Msg::ApplyPattern(term) => { 103 | self.cells = term 104 | .cells 105 | .iter() 106 | .fold(CellSet::new(), |cells, &cell| make_cell_alive(&cells, cell)); 107 | self.tick = 0; 108 | self.previous_gens = vec![]; 109 | self.offset = ( 110 | (self.width as f64 / 2_f64 111 | - term.width as f64 * self.zoom * (settings.cell_size + settings.grid_width) as f64 112 | / 2_f64), 113 | (self.height as f64 / 2_f64 114 | - term.height as f64 * self.zoom * (settings.cell_size + settings.grid_width) as f64 115 | / 2_f64), 116 | ); 117 | true 118 | } 119 | Msg::MoveOffset(offset) => { 120 | self.offset = offset; 121 | true 122 | } 123 | Msg::Resize => { 124 | let window = web_sys::window().unwrap(); 125 | let (width, height) = ( 126 | window.inner_width().unwrap().as_f64().unwrap() as u32, 127 | window.inner_height().unwrap().as_f64().unwrap() as u32, 128 | ); 129 | self.width = width; 130 | self.height = height; 131 | true 132 | } 133 | Msg::ChangeZoom((x1, y1, zoom)) => { 134 | let offset = self.offset; 135 | let prev_zoom = self.zoom; 136 | self.zoom = zoom; 137 | self.offset = ( 138 | offset.0 - (x1 as f64 - offset.0) * (self.zoom / prev_zoom - 1.0), 139 | offset.1 - (y1 as f64 - offset.1) * (self.zoom / prev_zoom - 1.0), 140 | ); 141 | true 142 | } 143 | } 144 | } 145 | 146 | fn create(ctx: &Context) -> Self { 147 | let window = web_sys::window().unwrap(); 148 | let link = ctx.link().clone(); 149 | let resize_handle = EventListener::new(&window, "resize", move |_: &Event| { 150 | link.send_message(Msg::Resize) 151 | }); 152 | 153 | Self { 154 | cells: CellSet::new(), 155 | previous_gens: vec![] as Vec, 156 | tick: 0, 157 | interval: None, 158 | speed: 5, 159 | adjust_offset: None, 160 | offset: (0.0, 0.0), 161 | zoom: 1.0, 162 | width: 300, 163 | height: 200, 164 | _resize_handle: resize_handle, 165 | } 166 | } 167 | 168 | fn rendered(&mut self, ctx: &Context, _first_render: bool) { 169 | if _first_render { 170 | ctx.link().send_message(Msg::Resize); 171 | } 172 | } 173 | 174 | fn view(&self, ctx: &Context) -> yew::virtual_dom::VNode { 175 | let running = self.interval.is_some(); 176 | 177 | let on_change_speed = ctx.link().callback(|event: Event| { 178 | let input = event 179 | .target() 180 | .and_then(|t| t.dyn_into::().ok()) 181 | .unwrap(); 182 | let speed: u8 = input.value().parse().unwrap(); 183 | Msg::ChangeSpeed(speed) 184 | }); 185 | 186 | let on_change_zoom = { 187 | let width = self.width; 188 | let height = self.height; 189 | ctx.link().callback(move |event: Event| { 190 | let input = event 191 | .target() 192 | .and_then(|t| t.dyn_into::().ok()) 193 | .unwrap(); 194 | let zoom: f64 = input.value().parse().unwrap(); 195 | Msg::ChangeZoom(( 196 | (width as f64 / 2_f64) as i32, 197 | (height as f64 / 2_f64) as i32, 198 | zoom, 199 | )) 200 | }) 201 | }; 202 | 203 | html! { 204 | <> 205 | 215 | 216 | 217 | {"Tick"} 218 | {{if running { "Pause" } else { "Play" }}} 225 | {format!("Generation #{}", self.tick)} 226 | 227 | 228 | 229 | {"Speed"} 230 | 235 | 236 | 237 | {"Zoom"} 238 | 243 | 244 | 245 | {"Made by "} 246 | {"Sébastien Castiel"} 247 | {" – "} 248 | {"About"} 249 | 250 | 251 | 252 | 253 | 254 | > 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.45" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7" 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.0.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 25 | 26 | [[package]] 27 | name = "bincode" 28 | version = "1.3.3" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 31 | dependencies = [ 32 | "serde", 33 | ] 34 | 35 | [[package]] 36 | name = "boolinator" 37 | version = "2.4.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" 40 | 41 | [[package]] 42 | name = "bumpalo" 43 | version = "3.8.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" 46 | 47 | [[package]] 48 | name = "cfg-if" 49 | version = "1.0.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 52 | 53 | [[package]] 54 | name = "console_error_panic_hook" 55 | version = "0.1.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 58 | dependencies = [ 59 | "cfg-if", 60 | "wasm-bindgen", 61 | ] 62 | 63 | [[package]] 64 | name = "gloo" 65 | version = "0.3.0" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "0b31ec63066de893f3be98da84af50441ea35819bd7be95373802dea56293952" 68 | dependencies = [ 69 | "gloo-console", 70 | "gloo-dialogs", 71 | "gloo-events", 72 | "gloo-file", 73 | "gloo-render", 74 | "gloo-storage", 75 | "gloo-timers", 76 | ] 77 | 78 | [[package]] 79 | name = "gloo-console" 80 | version = "0.1.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "770942b86a2ab86330201eeafc5fe526fb203e54dbc6ef82a36453cebcb90e4c" 83 | dependencies = [ 84 | "js-sys", 85 | "serde", 86 | "wasm-bindgen", 87 | "web-sys", 88 | ] 89 | 90 | [[package]] 91 | name = "gloo-dialogs" 92 | version = "0.1.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "4ffb557a2ea2ed283f1334423d303a336fad55fb8572d51ae488f828b1464b40" 95 | dependencies = [ 96 | "wasm-bindgen", 97 | "web-sys", 98 | ] 99 | 100 | [[package]] 101 | name = "gloo-events" 102 | version = "0.1.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "088514ec8ef284891c762c88a66b639b3a730134714692ee31829765c5bc814f" 105 | dependencies = [ 106 | "wasm-bindgen", 107 | "web-sys", 108 | ] 109 | 110 | [[package]] 111 | name = "gloo-file" 112 | version = "0.1.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "8f9fecfe46b5dc3cc46f58e98ba580cc714f2c93860796d002eb3527a465ef49" 115 | dependencies = [ 116 | "gloo-events", 117 | "js-sys", 118 | "wasm-bindgen", 119 | "web-sys", 120 | ] 121 | 122 | [[package]] 123 | name = "gloo-render" 124 | version = "0.1.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "5b4cda6e149df3bb4a3c6a343873903e5bcc2448a9877d61bb8274806ad67f6e" 127 | dependencies = [ 128 | "wasm-bindgen", 129 | "web-sys", 130 | ] 131 | 132 | [[package]] 133 | name = "gloo-storage" 134 | version = "0.1.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "8c6cbd4f8664a9eec3d1f65de3e88d5898ef331db56efceae30fae4883ed311b" 137 | dependencies = [ 138 | "js-sys", 139 | "serde", 140 | "serde_json", 141 | "thiserror", 142 | "wasm-bindgen", 143 | "web-sys", 144 | ] 145 | 146 | [[package]] 147 | name = "gloo-timers" 148 | version = "0.2.1" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" 151 | dependencies = [ 152 | "js-sys", 153 | "wasm-bindgen", 154 | "web-sys", 155 | ] 156 | 157 | [[package]] 158 | name = "hashbrown" 159 | version = "0.11.2" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 162 | 163 | [[package]] 164 | name = "indexmap" 165 | version = "1.7.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 168 | dependencies = [ 169 | "autocfg", 170 | "hashbrown", 171 | ] 172 | 173 | [[package]] 174 | name = "itoa" 175 | version = "0.4.8" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 178 | 179 | [[package]] 180 | name = "js-sys" 181 | version = "0.3.55" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" 184 | dependencies = [ 185 | "wasm-bindgen", 186 | ] 187 | 188 | [[package]] 189 | name = "lazy_static" 190 | version = "1.4.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 193 | 194 | [[package]] 195 | name = "lexicon" 196 | version = "0.1.2" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "32a7ee1bb3fcaf750bfe5dfa5153612491ecac62311c066a52a56e0298136e62" 199 | dependencies = [ 200 | "bincode", 201 | "regex", 202 | "serde", 203 | ] 204 | 205 | [[package]] 206 | name = "log" 207 | version = "0.4.14" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 210 | dependencies = [ 211 | "cfg-if", 212 | ] 213 | 214 | [[package]] 215 | name = "memchr" 216 | version = "2.4.1" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 219 | 220 | [[package]] 221 | name = "proc-macro2" 222 | version = "1.0.32" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" 225 | dependencies = [ 226 | "unicode-xid", 227 | ] 228 | 229 | [[package]] 230 | name = "quote" 231 | version = "1.0.10" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 234 | dependencies = [ 235 | "proc-macro2", 236 | ] 237 | 238 | [[package]] 239 | name = "regex" 240 | version = "1.5.4" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 243 | dependencies = [ 244 | "aho-corasick", 245 | "memchr", 246 | "regex-syntax", 247 | ] 248 | 249 | [[package]] 250 | name = "regex-syntax" 251 | version = "0.6.25" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 254 | 255 | [[package]] 256 | name = "ryu" 257 | version = "1.0.5" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 260 | 261 | [[package]] 262 | name = "scoped-tls-hkt" 263 | version = "0.1.2" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63" 266 | 267 | [[package]] 268 | name = "serde" 269 | version = "1.0.130" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" 272 | dependencies = [ 273 | "serde_derive", 274 | ] 275 | 276 | [[package]] 277 | name = "serde_derive" 278 | version = "1.0.130" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" 281 | dependencies = [ 282 | "proc-macro2", 283 | "quote", 284 | "syn", 285 | ] 286 | 287 | [[package]] 288 | name = "serde_json" 289 | version = "1.0.68" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" 292 | dependencies = [ 293 | "itoa", 294 | "ryu", 295 | "serde", 296 | ] 297 | 298 | [[package]] 299 | name = "slab" 300 | version = "0.4.5" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" 303 | 304 | [[package]] 305 | name = "syn" 306 | version = "1.0.81" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" 309 | dependencies = [ 310 | "proc-macro2", 311 | "quote", 312 | "unicode-xid", 313 | ] 314 | 315 | [[package]] 316 | name = "thiserror" 317 | version = "1.0.30" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 320 | dependencies = [ 321 | "thiserror-impl", 322 | ] 323 | 324 | [[package]] 325 | name = "thiserror-impl" 326 | version = "1.0.30" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 329 | dependencies = [ 330 | "proc-macro2", 331 | "quote", 332 | "syn", 333 | ] 334 | 335 | [[package]] 336 | name = "unicode-xid" 337 | version = "0.2.2" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 340 | 341 | [[package]] 342 | name = "wasm-bindgen" 343 | version = "0.2.78" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" 346 | dependencies = [ 347 | "cfg-if", 348 | "serde", 349 | "serde_json", 350 | "wasm-bindgen-macro", 351 | ] 352 | 353 | [[package]] 354 | name = "wasm-bindgen-backend" 355 | version = "0.2.78" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" 358 | dependencies = [ 359 | "bumpalo", 360 | "lazy_static", 361 | "log", 362 | "proc-macro2", 363 | "quote", 364 | "syn", 365 | "wasm-bindgen-shared", 366 | ] 367 | 368 | [[package]] 369 | name = "wasm-bindgen-futures" 370 | version = "0.4.28" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" 373 | dependencies = [ 374 | "cfg-if", 375 | "js-sys", 376 | "wasm-bindgen", 377 | "web-sys", 378 | ] 379 | 380 | [[package]] 381 | name = "wasm-bindgen-macro" 382 | version = "0.2.78" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" 385 | dependencies = [ 386 | "quote", 387 | "wasm-bindgen-macro-support", 388 | ] 389 | 390 | [[package]] 391 | name = "wasm-bindgen-macro-support" 392 | version = "0.2.78" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" 395 | dependencies = [ 396 | "proc-macro2", 397 | "quote", 398 | "syn", 399 | "wasm-bindgen-backend", 400 | "wasm-bindgen-shared", 401 | ] 402 | 403 | [[package]] 404 | name = "wasm-bindgen-shared" 405 | version = "0.2.78" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" 408 | 409 | [[package]] 410 | name = "web-sys" 411 | version = "0.3.55" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" 414 | dependencies = [ 415 | "js-sys", 416 | "wasm-bindgen", 417 | ] 418 | 419 | [[package]] 420 | name = "yew" 421 | version = "0.18.0" 422 | source = "git+https://github.com/yewstack/yew/#8fd61008743417b56f62a1aeb1134ce10d4f0da7" 423 | dependencies = [ 424 | "anyhow", 425 | "console_error_panic_hook", 426 | "gloo", 427 | "indexmap", 428 | "js-sys", 429 | "scoped-tls-hkt", 430 | "slab", 431 | "wasm-bindgen", 432 | "wasm-bindgen-futures", 433 | "web-sys", 434 | "yew-macro", 435 | ] 436 | 437 | [[package]] 438 | name = "yew-app" 439 | version = "0.1.0" 440 | dependencies = [ 441 | "bincode", 442 | "gloo", 443 | "lexicon", 444 | "serde", 445 | "wasm-bindgen", 446 | "web-sys", 447 | "yew", 448 | ] 449 | 450 | [[package]] 451 | name = "yew-macro" 452 | version = "0.18.0" 453 | source = "git+https://github.com/yewstack/yew/#8fd61008743417b56f62a1aeb1134ce10d4f0da7" 454 | dependencies = [ 455 | "boolinator", 456 | "lazy_static", 457 | "proc-macro2", 458 | "quote", 459 | "syn", 460 | ] 461 | --------------------------------------------------------------------------------