From 4bebc4d45e10625040d3d8f15f7ea659018b6c5d Mon Sep 17 00:00:00 2001 From: lsy Date: Sun, 20 Oct 2024 03:03:50 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=9F=E5=91=BD=E6=B8=B8=E6=88=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust/wasm/life_game/Cargo.toml | 16 ++++ rust/wasm/life_game/package.json | 20 ++++ rust/wasm/life_game/src/index.html | 31 ++++++ rust/wasm/life_game/src/index.js | 124 ++++++++++++++++++++++++ rust/wasm/life_game/src/lib.rs | 146 +++++++++++++++++++++++++++++ 5 files changed, 337 insertions(+) create mode 100644 rust/wasm/life_game/Cargo.toml create mode 100644 rust/wasm/life_game/package.json create mode 100644 rust/wasm/life_game/src/index.html create mode 100644 rust/wasm/life_game/src/index.js create mode 100644 rust/wasm/life_game/src/lib.rs diff --git a/rust/wasm/life_game/Cargo.toml b/rust/wasm/life_game/Cargo.toml new file mode 100644 index 0000000..7974ec9 --- /dev/null +++ b/rust/wasm/life_game/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wasm" +version = "0.1.0" +edition = "2021" + +[dependencies] +wasm-bindgen = "0.2.95" +js-sys = "0.3" + +[lib] +crate-type = ["cdylib", "rlib"] + + +[profile.release] +lto = true +opt-level = 'z' \ No newline at end of file diff --git a/rust/wasm/life_game/package.json b/rust/wasm/life_game/package.json new file mode 100644 index 0000000..aff9917 --- /dev/null +++ b/rust/wasm/life_game/package.json @@ -0,0 +1,20 @@ +{ + "name": "wasm", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "serve": "webpack-dev-server" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@lsy2246/life_game": "^0.1.0" + }, + "devDependencies": { + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.1.0" + } +} diff --git a/rust/wasm/life_game/src/index.html b/rust/wasm/life_game/src/index.html new file mode 100644 index 0000000..9b13e6f --- /dev/null +++ b/rust/wasm/life_game/src/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + + +
+ + +
+ + + diff --git a/rust/wasm/life_game/src/index.js b/rust/wasm/life_game/src/index.js new file mode 100644 index 0000000..cc2027e --- /dev/null +++ b/rust/wasm/life_game/src/index.js @@ -0,0 +1,124 @@ +import {Universe} from "@lsy2246/life_game"; + +const canvas = document.querySelector('#game-of-life-canvas'); +const universe = Universe.new(); + +let animationId = null; +const width = universe.width(); +const height = universe.height(); + +const CELL_SIZE = 5; // px +const GRID_COLOR = "#CCCCCC"; +const DEAD_COLOR = "#FFFFFF"; +const ALIVE_COLOR = "#000000"; + +canvas.height = (CELL_SIZE + 1) * height + 1; +canvas.width = (CELL_SIZE + 1) * width + 1; + +const ctx = canvas.getContext('2d'); + +const renderLoop = () => { + // 每一帧更新细胞状态 + universe.tick(); + + // 绘制网格和细胞 + drawGrid(); + drawCells(); + + // 请求下一帧 + animationId=requestAnimationFrame(renderLoop); +}; + +const drawGrid = () => { + ctx.beginPath(); + ctx.strokeStyle = GRID_COLOR; + + // 画垂直线 + 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); + } + + // 画水平线 + 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 drawCells = () => { + const cells = universe.get_cells(); + + ctx.beginPath(); + + // 遍历每个细胞,渲染它们的颜色 + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const idx = row * width + col; + const cell = cells[idx]; + + ctx.fillStyle = cell === 0 ? DEAD_COLOR : ALIVE_COLOR; + ctx.fillRect( + col * (CELL_SIZE + 1) + 1, + row * (CELL_SIZE + 1) + 1, + CELL_SIZE, + CELL_SIZE + ); + } + } + + ctx.stroke(); +}; + +// 开始游戏循环 +drawGrid(); +drawCells(); +requestAnimationFrame(renderLoop); + +const playPauseButton = document.getElementById("play-pause"); +const play = () => { + playPauseButton.textContent = "⏸"; + renderLoop(); +}; + +const pause = () => { + playPauseButton.textContent = "▶"; + cancelAnimationFrame(animationId); + animationId = null; +}; + +playPauseButton.addEventListener("click", event => { + if (animationId === null) { + play(); + } else { + pause(); + } +}); + + +canvas.addEventListener("click", event => { + const boundingRect = canvas.getBoundingClientRect(); + + const scaleX = canvas.width / boundingRect.width; + const scaleY = canvas.height / boundingRect.height; + + const canvasLeft = (event.clientX - boundingRect.left) * scaleX; + const canvasTop = (event.clientY - 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(); + drawCells(); +}); + +document.querySelector('#play-clear').addEventListener('click', () => { + pause(); + universe.clear_cells(); + drawGrid(); + drawCells(); +}) \ No newline at end of file diff --git a/rust/wasm/life_game/src/lib.rs b/rust/wasm/life_game/src/lib.rs new file mode 100644 index 0000000..b1910bc --- /dev/null +++ b/rust/wasm/life_game/src/lib.rs @@ -0,0 +1,146 @@ +extern crate wasm_bindgen; +extern crate js_sys; +use wasm_bindgen::prelude::*; + +use std::fmt; + +#[wasm_bindgen] +#[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 Universe { + fn get_index(&self, row: u32, column: u32) -> usize { + (row * self.width + column) as usize + } +} + +impl Universe { + pub fn live_neighbor_count(&self, row: u32, column: u32) -> u8 { + let mut count: u8 = 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.width; + let index = self.get_index(neighbor_row, neighbor_col); + count += self.cells[index] as u8; + } + } + count + } +} + +#[wasm_bindgen] +impl Universe { + 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); + next[idx] = match (cell, live_neighbors) { + (Cell::Alive, x) if x < 2 => Cell::Dead, + (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive, + (Cell::Alive, x) if x > 3 => Cell::Dead, + (Cell::Dead, 3) => Cell::Alive, + _ => cell, // 保持当前状态 + }; + } + } + self.cells = next; + } +} + +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(()) + } +} + +#[wasm_bindgen] +impl Universe { + pub fn new() -> Universe { + let width = 64; + let height = 64; + let cells = (0..width * height) + .map(|_i| { + if js_sys::Math::random() < 0.5 { + Cell::Alive + } else { + Cell::Dead + } + }).collect(); + Universe { + width, + height, + cells, + } + } + pub fn render(&self) -> String { + self.to_string() + } +} + +#[wasm_bindgen] +impl Universe { + pub fn width(&self) -> u32 { + self.width + } + + pub fn height(&self) -> u32 { + self.height + } + + pub fn cells(&self) -> *const Cell { + self.cells.as_ptr() + } + + pub fn get_cells(&self) -> Vec { + self.cells.iter().map(|&cell| cell as u8).collect() + } +} + +#[wasm_bindgen] +impl Universe { + pub fn set_width(&mut self, width: u32) { + self.width = width; + self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect(); + } + + pub fn set_height(&mut self, height: u32) { + self.height = height; + self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect(); + } + + pub fn toggle_cell(&mut self, row: u32, column: u32) { + let idx = self.get_index(row, column); + self.cells[idx] = match self.cells[idx]{ + Cell::Dead => Cell::Alive, + Cell::Alive => Cell::Dead, + }; + } + pub fn clear_cells(&mut self) { + self.cells = (0..self.width * self.height).map(|_i| Cell::Dead).collect(); + } +} +