diff --git a/src/main.zig b/src/main.zig index fa6b81b..56df003 100644 --- a/src/main.zig +++ b/src/main.zig @@ -25,12 +25,23 @@ var scene_height: u32 = 0; var paused: bool = false; var step: u32 = 0; +var dragging: bool = false; +var mouseX: u32 = 0; +var mouseY: u32 = 0; + +var SELECTED_PATTERN_BUFFER = [_]u8{ 'o', '!' } ++ [_]u8{0} ** 998; + // NOTE: Usage - // const msg = std.fmt.allocPrint(allocator, "", .{ }) catch return; // defer allocator.free(msg); // debug_print(msg.ptr, @intCast(msg.len)); extern fn debug_print(message: [*]const u8, length: u8) void; +export fn alloc(length: u8) [*]const u8 { + const memory = allocator.alloc(u8, length) catch unreachable; + return memory.ptr; +} + export fn set_window_dimensions(w: u32, h: u32) void { width = w; height = h; @@ -43,12 +54,30 @@ export fn get_output_buffer_pointer() *[OUTPUT_BUFFER_SIZE]u8 { return &OUTPUT_BUFFER; } -export fn pause() void { +export fn pause() bool { paused = !paused; + return paused; +} + +export fn clear() void { + @memset(&SCENE_BUFFER, 0); +} + +export fn click() void { + place_cells(); +} + +export fn set_dragging(isDragging: bool) void { + dragging = isDragging; +} + +export fn move_mouse(x: u32, y: u32) void { + mouseX = x; + mouseY = y; } -export fn mouse_click(x: u32, y: u32) void { - SCENE_BUFFER[(y / CELL_SIZE * scene_width + x / CELL_SIZE)] = 1; +export fn select_pattern(rle: [*]const u8) void { + @memcpy(&SELECTED_PATTERN_BUFFER, rle); } export fn setup() void { @@ -63,9 +92,61 @@ export fn setup() void { SCENE_BUFFER[(y * scene_width + x)] = 1; } +fn place_cells() void { + var dx: usize = 0; + var dy: usize = 0; + var run_count: usize = 0; + var cell_value: u8 = 0; + for (SELECTED_PATTERN_BUFFER) |code| { + switch (code) { + '0'...'9' => { + run_count *= 10; + run_count += code - '0'; + continue; + }, + 'o' => { + cell_value = 1; + }, + 'b' => { + cell_value = 0; + }, + '!' => { + break; + }, + '$' => { + dy += 1; + dx = 0; + run_count = 0; + continue; + }, + else => unreachable, + } + + for (0..@max(1, run_count)) |_| { + SCENE_BUFFER[(mouseY / CELL_SIZE + dy) * scene_width + mouseX / CELL_SIZE + dx] = cell_value; + dx += 1; + } + run_count = 0; + } +} + export fn draw() void { @memset(&OUTPUT_BUFFER, 0); + // Draw cursor + { + const sx = mouseX / CELL_SIZE; + const sy = mouseY / CELL_SIZE; + for (0..CELL_SIZE) |c_xi| { + for (0..CELL_SIZE) |c_yi| { + const x = sx * CELL_SIZE + c_xi; + const y = sy * CELL_SIZE + c_yi; + draw_pixel(x, y, .{ .r = 255, .g = 255, .b = 255, .a = 150 }); + } + } + } + + // Draw (live) cells for (0..scene_width * scene_height) |c_i| { if (SCENE_BUFFER[c_i] == 1) { const sx = c_i % scene_width; @@ -126,6 +207,10 @@ export fn draw() void { SCENE_BUFFER = TEMP_SCENE_BUFFER; step += 1; } + + if (dragging) { + place_cells(); + } } fn draw_pixel(x: u32, y: u32, c: Color) void { diff --git a/web/bin/zonzai.wasm b/web/bin/zonzai.wasm index aca7723..eae8bc9 100755 Binary files a/web/bin/zonzai.wasm and b/web/bin/zonzai.wasm differ diff --git a/web/index.html b/web/index.html index a9b757c..7fb63c5 100644 --- a/web/index.html +++ b/web/index.html @@ -7,24 +7,41 @@ + + + + -

- - +
+
+

Zonzai

+
+ +
+
+ + +
+
+

+
+
-

- This is Conway's game of life, cells can be placed with the mouse. I'll - next add a menu of pre-made patterns that can be placed. -

-

- There was a tree simulator here, but I removed it in the transition to a - cellular automata approach. Simulating plants is very difficult, yet I - just had the idea of a CA where cells store resources (like water) and - transfer them out to neighbors with osmosis. If a cell has an abundance of - nutrients it could grow a neighbor and if it has a derth it will die. - Still need to figure out how to model light and branching. -

+
+

This is Conway's game of life. Cells can be placed with the mouse.

+

+ There was a tree simulator here, but I removed it in the transition to a + cellular automata approach. Simulating plants is very difficult, yet I + just had the idea of a CA where cells store resources (like water) and + transfer them out to neighbors with osmosis. If a cell has an abundance + of nutrients it could grow a neighbor and if it has a derth it will die. + Still need to figure out how to model light and branching. +

+
diff --git a/web/main.js b/web/main.js index aa08b67..b03a88b 100644 --- a/web/main.js +++ b/web/main.js @@ -3,6 +3,30 @@ let canvas let canvasContext let canvasImageData +let dragging +let mousedown + +const CELL_SIZE = 5 + +const EXAMPLE_PATTERNS = [ + { + name: "Cell", + rle: "o!", + }, + { + name: "Glider", + rle: "bob$2bo$3o!", + }, + { + name: "70P23", + rle: "24bo$12bo9b3o$o11b3o6bo$3o5b2o5bo5b2o$3bo4b2o4b2o$2b2o$19bobo$19bobo$3b2o14b3o3b2o$3b2o3b3o14b2o$8bobo$8bobo$26b2o$14b2o4b2o4bo$7b2o5bo5b2o5b3o$8bo6b3o11bo$5b3o9bo$5bo!", + }, + { + name: "p24 gliderless LWSS gun", + rle: "24b2o3b2o$2b2obo2bo15bobobo2bo$o2bob4o17bob4o$2o24bo2bo2b2o$4bob2obo9b2o3bobo3b2o2bo$2ob4ob2o9bo3b3o7b2o$o3bo12bobobob2o$b4o11bobo2bob2o$5b2o3b2o4bobob3o$3b4o3bobob2obobo$2bo3bo5bobo2bo12bo2bobo$bobo8bo3b2o10b2obob3o$bo2b2o3b3o8b2o7bo6bo$2b2obo4b2o8b2o6b2o5bo$5b3o4b3o5bo$6b2ob2o3b2o5bo7bo5b2o$b2ob2obob2o3b2o6b2o4bo6bo$obobo9b2o6b3o4b3obob2o$bo3b6o3bo14bobo2bo$7bo3bo9b2o$10bo10bo$10bo3bo6bo$6bo8bo$7b2o2bo3bo6bo2bo8bo2bo$5b2o9b2o8bo11bo$7bo5bo4bo3bo3bo7bo3bo$12bobo3bo4b4o8b4o$13bo4bo$7b2o3bo3b2o$6bobo3bo2bo$6bo4bo3bo$5b2o5bo2$8b2o8b2o$8bo2b6o2bo$9b2o6b2o$6b3o10b3o$6bo2bobo4bobo2bo$7b2o4b2o4b2o!", + }, +] + const debug_print = (location, size) => { var buffer = new Uint8Array(instance.exports.memory.buffer, location, size) var decoder = new TextDecoder() @@ -14,29 +38,55 @@ let frameTime = performance.now() let lastFpsUpdateMS = new Date().getTime() let prevFrameIntervals = [] +const patternPaletteElement = document.getElementById("pattern-palette") const fpsElement = document.getElementById("fps") -const pauseBtnElement = document.getElementById("pauseBtn") +const pauseBtnElement = document.getElementById("pause-btn") +const clearBtnElement = document.getElementById("clear-btn") pauseBtnElement.addEventListener("click", () => { - instance.exports.pause() + const pauseResult = instance.exports.pause() + pauseBtnElement.innerText = pauseResult ? "unpause" : "pause" +}) + +clearBtnElement.addEventListener("click", () => { + instance.exports.clear() }) const setCanvasDimensions = () => { - canvas.width = window.innerWidth - canvas.height = window.innerHeight + canvas.width = Math.floor((window.innerWidth * 0.96) / CELL_SIZE) * CELL_SIZE + canvas.height = Math.floor((window.innerHeight - 240) / CELL_SIZE) * CELL_SIZE instance.exports.set_window_dimensions(canvas.width, canvas.height) canvasImageData = canvasContext.createImageData(canvas.width, canvas.height) } window.addEventListener("resize", setCanvasDimensions) -const memory = new WebAssembly.Memory({ - initial: 1000, - maximum: 65536, +EXAMPLE_PATTERNS.forEach((p) => { + const patternButtonElement = document.createElement("button") + patternButtonElement.classList.add("btn") + patternButtonElement.innerText = p.name + patternButtonElement.addEventListener("click", () => { + // TODO: There's definitely a better way to do this w/o allocating + // cause I just copy it into the same fixed buffer + const rleBuffer = Uint8Array.from( + Array.from(p.rle).map((letter) => letter.charCodeAt(0)), + ) + + var ptr = instance.exports.alloc(rleBuffer.length) + var mem = new Uint8Array( + instance.exports.memory.buffer, + ptr, + rleBuffer.length, + ) + mem.set(new Uint8Array(rleBuffer)) + + instance.exports.select_pattern(ptr) + console.log(p.rle, p.rle.length) + }) + patternPaletteElement.appendChild(patternButtonElement) }) WebAssembly.instantiateStreaming(fetch("./bin/zonzai.wasm"), { - js: { mem: memory }, env: { debug_print: debug_print, }, @@ -47,18 +97,43 @@ WebAssembly.instantiateStreaming(fetch("./bin/zonzai.wasm"), { canvasContext = canvas.getContext("2d") setCanvasDimensions() - let mousedown = false + let mousePosition = { x: 0, y: 0 } + + canvas.addEventListener("mouseup", (e) => { + mousedown = false + dragging = false + instance.exports.set_dragging(false) + }) canvas.addEventListener("mousedown", (e) => { mousedown = true - instance.exports.mouse_click(e.x, canvas.height - e.y) }) - canvas.addEventListener("mouseup", (e) => { - mousedown = false + + canvas.addEventListener("click", (e) => { + instance.exports.click() }) + canvas.addEventListener("mousemove", (e) => { - if (mousedown) { - instance.exports.mouse_click(e.x, canvas.height - e.y) + if (dragging) { + instance.exports.set_dragging(true) + } + + let canvasBoundingRect = canvas.getBoundingClientRect() + const newMousePosition = { + x: e.x - canvasBoundingRect.left, + y: canvas.height + canvasBoundingRect.top - e.y, + } + if ( + mousePosition.x !== newMousePosition.x || + mousePosition.y !== newMousePosition.y + ) { + if (mousedown) { + dragging = true + } else { + dragging = false + } + instance.exports.move_mouse(newMousePosition.x, newMousePosition.y) + mousePosition = newMousePosition } }) diff --git a/web/style.css b/web/style.css index 8d50711..61d713f 100644 --- a/web/style.css +++ b/web/style.css @@ -1,9 +1,15 @@ * { user-select: none; + + font-family: "Pixelify Sans", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + color: white; } :root { - background-color: black; + background-color: rebeccapurple; } body { @@ -11,28 +17,71 @@ body { } .canvas { - height: 100vh; - width: 100vw; + background-color: black; image-rendering: pixelated; - flex: 1; +} + +.gameContainer { + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; +} + +.gameHeader { + height: 80px; +} + +.gameFooter { + height: 120px; + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + column-gap: 20px; + flex-wrap: wrap; +} + +.gameControls { + display: flex; + flex-direction: row; + align-items: center; + gap: 20px; +} + +#pattern-palette { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; } #fps { - position: absolute; - top: 0; - right: 0; - background-color: black; - padding: 12px; - margin: 0; color: white; + width: 80px; + text-align: center; } -#pauseBtn { - position: absolute; - top: 0; - left: 0; - background-color: black; +#pause-btn { + width: 80px; +} + +.btn { padding: 12px; - margin: 0; color: white; + background-color: #442277; + border: none; + border-radius: 6px; + box-shadow: 1px 1px 5px rgb(0 0 0 / 15%); +} + +.btn:hover { + background-color: #402066; +} + +.description { + max-width: 640px; + margin: 0 auto; + padding: 0 3vw 40px 3vw; }