Mandelbrot set

Controls: arrow keys to pan, X/Z to zoom

catridge

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);
}