Mandelbrot set
Controls: arrow keys to pan, X/Z to zoom
Code in Zig
// zig build && w4 run zig-out/lib/mandelbrot_wasm4.wasm --no-open
const std = @import("std");
const math = std.math;
const assert = std.debug.assert;
const expect = std.testing.expect;
const Complex = std.math.Complex(f32);
const jaese = @import("jaese");
const w4 = jaese.lib.games.wasm4.w4;
const App = struct{
buffer: [100]u8 = undefined,
fba: std.heap.FixedBufferAllocator,
allocator: *std.mem.Allocator,
framebuffer: [6400]u8,
span: f32,
offset_x: f32,
offset_y: f32,
};
var app: App = .{
.buffer = undefined,
.fba = undefined,
.allocator = undefined,
.framebuffer = undefined,
.span = 4.0,
.offset_x = 0.0,
.offset_y = 0.0,
};
export fn start() void {
app.fba = std.heap.FixedBufferAllocator.init(&app.buffer);
app.allocator = &app.fba.allocator;
const z_0 = Complex{ .re = 0, .im = 0 };
mandelbrot(z_0, app.span, app.offset_x, app.offset_y);
}
export fn update() void {
const move_step = app.span / 100;
const zoom_step = app.span / 100;
var redraw = false;
if (w4.GAMEPAD1.* & w4.BUTTON_DOWN != 0) {
app.offset_y += move_step;
redraw = true;
}
if (w4.GAMEPAD1.* & w4.BUTTON_UP != 0) {
app.offset_y -= move_step;
redraw = true;
}
if (w4.GAMEPAD1.* & w4.BUTTON_LEFT != 0) {
app.offset_x -= move_step;
redraw = true;
}
if (w4.GAMEPAD1.* & w4.BUTTON_RIGHT != 0) {
app.offset_x += move_step;
redraw = true;
}
if (w4.GAMEPAD1.* & w4.BUTTON_1 != 0) {
app.span -= zoom_step;
redraw = true;
}
if (w4.GAMEPAD1.* & w4.BUTTON_2 != 0) {
app.span += zoom_step;
redraw = true;
}
if (redraw) {
const z_0 = Complex{ .re = 0, .im = 0 };
mandelbrot(z_0, app.span, app.offset_x, app.offset_y);
}
blit_framebuffer();
w4.DRAW_COLORS.* = 3;
const span_text = std.fmt.allocPrint(
app.allocator,
"span = {d}",
.{ app.span },
) catch unreachable;
defer app.allocator.free(span_text);
w4.text(span_text, 0, 160 - 10*2);
const coords_text = std.fmt.allocPrint(
app.allocator,
"c = {d:.2} + {d:.2}i",
.{ app.offset_x, app.offset_y },
) catch unreachable;
defer app.allocator.free(coords_text);
w4.text(coords_text, 0, 160 - 10);
}
fn blit_framebuffer() void {
var i: usize = 0;
while (i < 6400) : (i += 1) {
w4.FRAMEBUFFER[i] = app.framebuffer[i];
}
}
fn mandelbrot(z_0: Complex, span: f32, offset_x: f32, offset_y: f32) void {
const n = w4.CANVAS_SIZE;
const step: f32 = span / @intToFloat(f32, n);
var i: usize = 0;
var re: f32 = -(span / 2) + offset_x;
while (i < n) : (i += 1) {
var j: usize = 0;
var im: f32 = -(span / 2) + offset_y;
while (j < n) : (j += 1) {
const c = Complex{ .re = re, .im = im };
const m = convergence(z_0, c);
var color: u2 = undefined;
if (m >= 75) {
color = 3;
} else if (m >= 50) {
color = 2;
} else if (m >= 25) {
color = 1;
} else {
color = 0;
}
write_pixel(&app.framebuffer, i, j, color);
im += step;
}
re += step;
}
}
fn convergence(z_0: Complex, c: Complex) u8 {
const limit: f32 = 1000;
var z = z_0;
var i: u8 = 0;
while (i < 100) : (i += 1) {
z = z.mul(z).add(c);
if (z.magnitude() > limit) {
break;
}
}
return i;
}
pub fn write_pixel(framebuffer: *[6400]u8, x: u32, y: u32, color: u2) void {
const idx = (y * w4.CANVAS_SIZE + x) >> 2;
const shift = @intCast(u3, (x & 0b11) * 2);
const mask = @as(u8, 0b11) << shift;
framebuffer[idx] = (@intCast(u8, color) << shift) | (framebuffer[idx] & ~mask);
}