diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10764bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/zig-cache +/zig-out \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..07d276c --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# ぞんざい +[d͡zõ̞nd͡za̠i] + + +***Adjective*** +1. markedly rough or careless in manner + +***Noun (1)*** +1. person whose words and actions are violent +2. state of being rude + +***Noun (2)*** +1. bonsai tree generator \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..a5b63f6 --- /dev/null +++ b/build.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + const exe = b.addExecutable(.{ + .name = "zonzai", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + exe.rdynamic = true; + exe.entry = .disabled; + exe.use_llvm = false; + exe.use_lld = false; + + const install = b.addInstallArtifact(exe, .{}); + install.step.dependOn(&exe.step); + b.default_step.dependOn(&install.step); + + // const lib_unit_tests = b.addTest(.{ + // .root_source_file = .{ .path = "src/main.zig" }, + // .target = target, + // .optimize = optimize, + // }); + + // const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + // // Similar to creating the run step earlier, this exposes a `test` step to + // // the `zig build --help` menu, providing a way for the user to request + // // running the unit tests. + // const test_step = b.step("test", "Run unit tests"); + // test_step.dependOn(&run_lib_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..b180c56 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,62 @@ +.{ + .name = "zonzai", + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.1.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + // This makes *all* files, recursively, included in this package. It is generally + // better to explicitly list the files and directories instead, to insure that + // fetching from tarballs, file system paths, and version control all result + // in the same contents hash. + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..fa90ba1 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,267 @@ +const std = @import("std"); + +const Vec3 = struct { x: f32, y: f32, z: f32}; +const Color = struct { r: u8, g: u8, b: u8, a: u8 }; + +const Branch = struct { + parent: ?*Branch, + length: f32, + max_length: f32, + diameter: f32, + direction: Vec3, + growth_rate: f32, + children: std.ArrayList(*Branch), + leaves: std.ArrayList(Leaf), +}; + +const Leaf = struct { + size: f32, + position: f32, + color: Color, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +const allocator = gpa.allocator(); + +var trunk: Branch = Branch{ + .parent = null, + .length = 0.1, + .max_length = 150, + .diameter = 0.1, + .direction = Vec3{ .x = 0, .y = 0, .z = 1 }, + .growth_rate = 1, + .children = std.ArrayList(*Branch).init(allocator), + .leaves = std.ArrayList(Leaf).init(allocator) +}; + +const MAX_WIDTH = 4096; +const MAX_HEIGHT = 4096; + +const OUTPUT_BUFFER_SIZE: u32 = MAX_WIDTH * MAX_HEIGHT * 4; +var OUTPUT_BUFFER = [_:0]u8{0} ** OUTPUT_BUFFER_SIZE; + +var width: u32 = 0; +var height: u32 = 0; + +var prng = std.rand.DefaultPrng.init(0); +var random = prng.random(); +var rand_seed: u64 = 0; + +var hour: u32 = 0; + +extern fn debug_print(message: [*]const u8, length: u8) void; + +export fn set_rand_seed(s: u64) void { + prng = std.rand.DefaultPrng.init(s); + random = prng.random(); +} + +export fn set_window_dimensions(w: u8, h: u8) void { + width = w; + height = h; +} + +export fn get_output_buffer_pointer() *[OUTPUT_BUFFER_SIZE]u8 { + return &OUTPUT_BUFFER; +} + +var last_pixels_set = std.ArrayList(u32).init(allocator); + +fn draw_pixel(x: u32, y: u32, c: Color) void { + if (y < height and x < width) { + const pixel: u32 = (height - y - 1) * width + x; + const pixel_offset: u32 = pixel * 4; + + last_pixels_set.append(pixel_offset) catch return; + + OUTPUT_BUFFER[pixel_offset] = c.r; + OUTPUT_BUFFER[pixel_offset + 1] = c.g; + OUTPUT_BUFFER[pixel_offset + 2] = c.b; + OUTPUT_BUFFER[pixel_offset + 3] = c.a; + } +} + +fn draw_line(x0: i32, y0: i32, x1: i32, y1: i32) void { + var x = x0; + var y = y0; + const dx: i32 = @intCast(@abs(x1 - x0)); + const dy: i32 = -@as(i32, @intCast(@abs(y1 - y0))); + const sx: i32 = if (x0 < x1) 1 else -1; + const sy: i32 = if (y0 < y1) 1 else -1; + var err = dx + dy; + + while (true) { + if (x >= 0 and y >= 0) { + draw_pixel(@intCast(x), @intCast(y), Color{ .r = 255, .g = 255, .b = 255, .a = 255 }); + } + if (x == x1 and y == y1) break; + const e2 = 2 * err; + if (e2 >= dy) { err += dy; x += sx; } + if (e2 <= dx) { err += dx; y += sy; } + } +} + +fn draw_horizontal_line(x1: i32, x2: i32, y: i32, color: Color) void { + var x = x1; + while (x <= x2) : (x += 1) { + if (x >= 0 and y >= 0) { + draw_pixel(@intCast(x), @intCast(y), color); + } + } +} + +fn draw_circle(xc: i32, yc: i32, r: f32, color: Color) void { + var x: i32 = 0; + var y: i32 = @intFromFloat(r); + var d: f32 = 3 - 2 * r; + + draw_horizontal_line(xc - y, xc + y, yc, color); + + while (y >= x) { + x += 1; + + if (d > 0) { + y -= 1; + d = d + 4 * @as(f32, @floatFromInt(x - y)) + 10; + } else { + d = d + 4 * @as(f32, @floatFromInt(x)) + 6; + } + + draw_horizontal_line(xc - x, xc + x, yc + y, color); + draw_horizontal_line(xc - x, xc + x, yc - y, color); + draw_horizontal_line(xc - y, xc + y, yc + x, color); + draw_horizontal_line(xc - y, xc + y, yc - x, color); + } +} + +fn render_branch(branch: *Branch, baseX: f32, baseZ: f32) void { + const endX = baseX + branch.length * branch.direction.x; + const endZ = baseZ + branch.length * branch.direction.z; + + draw_line(@intFromFloat(baseX), @intFromFloat(baseZ), @intFromFloat(endX), @intFromFloat(endZ)); + + for (branch.children.items) |child| { + render_branch(child, endX, endZ); + } + + for (branch.leaves.items) |leaf| { + draw_circle( + @intFromFloat(baseX + branch.length * branch.direction.x * leaf.position), + @intFromFloat(baseZ + branch.length * branch.direction.z * leaf.position), + leaf.size, + leaf.color + ); + } +} + +fn render_tree() void { + for (last_pixels_set.items) |pixel_offset| { + OUTPUT_BUFFER[pixel_offset] = 0; + OUTPUT_BUFFER[pixel_offset + 1] = 0; + OUTPUT_BUFFER[pixel_offset + 2] = 0; + OUTPUT_BUFFER[pixel_offset + 3] = 0; + } + + last_pixels_set.clearRetainingCapacity(); + + const baseX: f32 = @as(f32, @floatFromInt(width)) / 2.0; + const baseY: f32 = 0.0; + + render_branch(&trunk, baseX, baseY); +} + +fn destroy_branch(branch: *Branch) void { + while (branch.children.items.len > 0) { + const child = branch.children.pop(); + destroy_branch(child); + } + + while (branch.leaves.items.len > 0) { + _ = branch.leaves.pop(); + } + + allocator.destroy(branch); +} + +fn grow_branch(branch: *Branch, depth: u8, grew_new_branch: *bool) void { + if (std.rand.Random.float(random, f32) < 0.001 and branch.parent != null and branch.children.items.len > 0) { + const child = branch.children.pop(); + destroy_branch(child); + } + + if (branch.length < branch.max_length) { + branch.length += branch.growth_rate; + } + + const day_of_year = (hour / 24) % 365; + if (day_of_year > 60 and day_of_year < 260) { + if (branch.leaves.items.len <= 1 and std.rand.Random.float(random, f32) < 0.005 and branch.parent != null) { + const new_leaf = Leaf { + .position = std.rand.Random.float(random, f32), + .color = Color{ .r = 255, .g = 183 + @as(u8, @intFromFloat(std.rand.Random.float(random, f32) * 20)), .b = 197 + @as(u8, @intFromFloat(std.rand.Random.float(random, f32) * 20)), .a = 255 }, + .size = std.rand.Random.float(random, f32) * 10, + }; + + branch.leaves.append(new_leaf) catch return; + } + } else { + if (branch.leaves.items.len > 0 and std.rand.Random.float(random, f32) < 0.05) { + _ = branch.leaves.pop(); + } + } + + if (!grew_new_branch.* and depth <= 3 and branch.children.items.len <= 3 and std.rand.Random.float(random, f32) < 0.05) { + grew_new_branch.* = true; + const new_branch_ptr = allocator.create(Branch) catch return; + + const r0 = std.rand.Random.float(random, f32); + const r1 = std.rand.Random.float(random, f32); + + new_branch_ptr.* = Branch{ + .parent = &trunk, + .length = 0.1, + .max_length = std.rand.Random.float(random, f32) * 200, + .diameter = 0.1, + .direction = Vec3{ .x = r0 * 2.0 - 1.0, .y = 0, .z = r1 * 1.5 - 0.5 }, + .growth_rate = std.rand.Random.float(random, f32), + .children = std.ArrayList(*Branch).init(allocator), + .leaves = std.ArrayList(Leaf).init(allocator), + }; + + branch.children.append(new_branch_ptr) catch return; + } + + for (branch.children.items) |child| { + grow_branch(child, depth + 1, grew_new_branch); + } +} + +export fn grow_tree() void { + var grew_new_branch = false; + // const random_number = std.fmt.allocPrint(allocator, "random number: {any}", .{f}) catch "failed to create string"; + // debug_print(@ptrCast(random_number), @intCast(random_number.len)); + + grow_branch(&trunk, 0, &grew_new_branch); + hour += 1; + + render_tree(); +} + +pub const os = struct { + pub const system = struct { + pub const fd_t = u8; + pub const STDERR_FILENO = 1; + pub const E = std.os.linux.E; + + pub fn getErrno(T: u32) E { + _ = T; + return .SUCCESS; + } + + pub fn write(f: fd_t, ptr: [*]const u8, len: u32) u32 { + _ = ptr; + _ = f; + return len; + } + }; +}; diff --git a/web/bin/zonzai.wasm b/web/bin/zonzai.wasm new file mode 100644 index 0000000..05622fb Binary files /dev/null and b/web/bin/zonzai.wasm differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..a0b41e3 --- /dev/null +++ b/web/index.html @@ -0,0 +1,21 @@ + + + + + + + Zonzai + + + + + + +
+

+
+ + + + + \ No newline at end of file diff --git a/web/main.js b/web/main.js new file mode 100644 index 0000000..7339f1c --- /dev/null +++ b/web/main.js @@ -0,0 +1,71 @@ +let instance; +let canvas; +let canvasContext; +let canvasImageData; + +const debug_print = (location, size) => { + var buffer = new Uint8Array(instance.exports.memory.buffer, location, size); + var decoder = new TextDecoder(); + var string = decoder.decode(buffer); + console.log(string); +} + +let frameTime = performance.now() + +const fpsElement = document.getElementById('fps') + +const setCanvasDimensions = () => { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + instance.exports.set_window_dimensions(canvas.width, canvas.height); + canvasImageData = canvasContext.createImageData( + canvas.width, + canvas.height + ) +} + +window.addEventListener('resize', setCanvasDimensions) + +WebAssembly.instantiateStreaming(fetch("./bin/zonzai.wasm"), { + env: { + debug_print: debug_print, + } +}).then(res => { + instance = res.instance; + + canvas = document.querySelector("canvas"); + canvasContext = canvas.getContext("2d"); + setCanvasDimensions() + + instance.exports.set_rand_seed(BigInt(new Date().getTime())); + + const draw = () => { + instance.exports.grow_tree(); + + const outputPointer = instance.exports.get_output_buffer_pointer(); + + const imageDataArray = new Uint8Array(instance.exports.memory.buffer).slice( + outputPointer, + outputPointer + canvas.width * canvas.height * 4 + ); + + if (imageDataArray.length <= canvasImageData.data.length) + canvasImageData.data.set(imageDataArray); + + canvasContext.clearRect(0, 0, canvas.width, canvas.height); + canvasContext.putImageData(canvasImageData, 0, 0); + + let newFrameTime = performance.now() + if (fpsElement) + fpsElement.innerText = `${(1000 / ((newFrameTime - frameTime))).toFixed(1)} fps` + frameTime = newFrameTime + + // window.requestAnimationFrame(draw) + }; + + setInterval(() => { + draw() + }, 1); + + draw(); +}); \ No newline at end of file diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..61d87c1 --- /dev/null +++ b/web/style.css @@ -0,0 +1,20 @@ +:root { + background-color: black; +} + +body { + margin: 0 +} + +.canvas { + image-rendering: pixelated; + flex: 1; +} + +.perfStats { + position: absolute; + top: 10; + right: 10; + background-color: black; + color: white +} \ No newline at end of file