diff --git a/showcase/build.zig b/showcase/build.zig index 9c95f92..64734f9 100644 --- a/showcase/build.zig +++ b/showcase/build.zig @@ -10,6 +10,7 @@ const carts = .{ .{ "metalgear_timer", @import("metalgear_timer") }, .{ "raytracer", @import("raytracer") }, .{ "neopixelpuzzle", @import("neopixelpuzzle") }, + .{ "dvd", @import("dvd") }, }; pub fn build(b: *std.Build) void { diff --git a/showcase/build.zig.zon b/showcase/build.zig.zon index e6d9d0c..a719727 100644 --- a/showcase/build.zig.zon +++ b/showcase/build.zig.zon @@ -16,6 +16,7 @@ .metalgear_timer = .{ .path = "carts/metalgear-timer" }, .raytracer = .{ .path = "carts/raytracer" }, .neopixelpuzzle = .{ .path = "carts/neopixelpuzzle" }, + .dvd = .{ .path = "carts/dvd" }, }, .paths = .{ "README.md", diff --git a/showcase/carts/dvd/assets/dvd.png b/showcase/carts/dvd/assets/dvd.png new file mode 100644 index 0000000..0ce5e84 Binary files /dev/null and b/showcase/carts/dvd/assets/dvd.png differ diff --git a/showcase/carts/dvd/build.zig b/showcase/carts/dvd/build.zig new file mode 100644 index 0000000..54245a7 --- /dev/null +++ b/showcase/carts/dvd/build.zig @@ -0,0 +1,55 @@ +const std = @import("std"); +const Build = std.Build; +const sycl_badge = @import("sycl_badge"); + +pub const author_name = "Stevie Hryciw"; +pub const author_handle = "hryx"; +pub const cart_title = "dvd"; +pub const description = "Bouncing DVD logo screensaver"; + +pub fn build(b: *Build) void { + const optimize = b.standardOptimizeOption(.{}); + const sycl_badge_dep = b.dependency("sycl_badge", .{}); + + const cart = sycl_badge.add_cart(sycl_badge_dep, b, .{ + .name = "dvd", + .optimize = optimize, + .root_source_file = b.path("src/main.zig"), + }); + add_dvd_assets_step(b, sycl_badge_dep, cart); + cart.install(b); +} + +// Thank you to Fabio for the code generation step. +fn add_dvd_assets_step( + b: *Build, + sycl_badge_dep: *Build.Dependency, + cart: *sycl_badge.Cart, +) void { + const convert = b.addExecutable(.{ + .name = "convert_gfx", + .root_source_file = b.path("build/convert_gfx.zig"), + .target = b.host, + .optimize = cart.options.optimize, + .link_libc = true, + }); + convert.root_module.addImport("zigimg", b.dependency("zigimg", .{}).module("zigimg")); + + const gen_gfx = b.addRunArtifact(convert); + gen_gfx.addArg("-i"); + gen_gfx.addFileArg(b.path("assets/dvd.png")); + gen_gfx.addArg(std.fmt.comptimePrint("{}", .{8})); + gen_gfx.addArg(std.fmt.comptimePrint("{}", .{false})); + gen_gfx.addArg("-o"); + const gfx_zig = gen_gfx.addOutputFileArg("gfx.zig"); + + const gfx_mod = b.addModule("gfx", .{ + .root_source_file = gfx_zig, + .optimize = cart.options.optimize, + }); + gfx_mod.addImport("cart-api", sycl_badge_dep.module("cart-api")); + + cart.wasm.step.dependOn(&gen_gfx.step); + cart.wasm.root_module.addImport("gfx", gfx_mod); + cart.cart_lib.root_module.addImport("gfx", gfx_mod); +} diff --git a/showcase/carts/dvd/build.zig.zon b/showcase/carts/dvd/build.zig.zon new file mode 100644 index 0000000..f6e4329 --- /dev/null +++ b/showcase/carts/dvd/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = "dvd", + .version = "0.0.0", + .dependencies = .{ + .sycl_badge = .{ .path = "../../.." }, + .zigimg = .{ + .url = "https://github.com/zigimg/zigimg/archive/637974e2d31dcdbc33f1e9cc8ffb2e46abd2e215.tar.gz", + .hash = "122012026c3a65ff1d4acba3b3fe80785f7cee9c6b4cdaff7ed0fbf23b0a6c803989", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "build", + "assets", + }, +} diff --git a/showcase/carts/dvd/build/convert_gfx.zig b/showcase/carts/dvd/build/convert_gfx.zig new file mode 100644 index 0000000..d64d1ef --- /dev/null +++ b/showcase/carts/dvd/build/convert_gfx.zig @@ -0,0 +1,114 @@ +const std = @import("std"); +const allocator = std.heap.c_allocator; +const Image = @import("zigimg").Image; + +const ConvertFile = struct { + path: []const u8, + bits: u4, + transparency: bool, +}; + +pub fn main() !void { + var args = try std.process.argsWithAllocator(allocator); + defer args.deinit(); + + _ = args.next(); + var in_files = std.ArrayList(ConvertFile).init(allocator); + var out_path: []const u8 = undefined; + while (args.next()) |arg| { + if (std.mem.eql(u8, arg, "-i")) { + const path = args.next() orelse return error.MissingArg; + const bits = args.next() orelse return error.MissingArg; + const transparency = args.next() orelse return error.MissingArg; + try in_files.append(.{ .path = path, .bits = @intCast(bits[0] - '0'), .transparency = transparency[0] == 't' }); + } else if (std.mem.eql(u8, arg, "-o")) { + out_path = args.next() orelse return error.MissingArg; + } + } + + const out_file = try std.fs.cwd().createFile(out_path, .{}); + defer out_file.close(); + + const writer = out_file.writer(); + try writer.writeAll("const PackedIntSlice = @import(\"std\").packed_int_array.PackedIntSlice;\n"); + try writer.writeAll("const DisplayColor = @import(\"cart-api\").DisplayColor;\n\n"); + + for (in_files.items) |in_file| { + try convert(in_file, writer); + } +} + +fn convert(args: ConvertFile, writer: std.fs.File.Writer) !void { + const N = 8 / args.bits; + + var image = try Image.fromFilePath(allocator, args.path); + defer image.deinit(); + + var colors = std.ArrayList(Color).init(allocator); + defer colors.deinit(); + if (args.transparency) try colors.append(.{ .r = 31, .g = 0, .b = 31 }); + var indices = try std.ArrayList(usize).initCapacity(allocator, image.width * image.height); + defer indices.deinit(); + var it = image.iterator(); + while (it.next()) |pixel| { + const color = Color{ + .r = @intFromFloat(31.0 * pixel.r), + .g = @intFromFloat(63.0 * pixel.g), + .b = @intFromFloat(31.0 * pixel.b), + }; + const index = try getIndex(&colors, color); + indices.appendAssumeCapacity(index); + } + var packed_data = try allocator.alloc(u8, indices.items.len / N); + defer allocator.free(packed_data); + for (packed_data, 0..) |_, i| { + packed_data[i] = 0; + for (0..N) |n| { + const shift: u3 = @intCast(n * args.bits); + packed_data[i] |= @intCast(indices.items[N * i + n] << shift); + } + } + + { + const name = std.fs.path.stem(args.path); + try writer.print("pub const {s} = struct {{\n", .{name}); + + try writer.print(" pub const width = {};\n", .{image.width}); + try writer.print(" pub const height = {};\n", .{image.height}); + + try writer.writeAll(" pub const colors = [_]DisplayColor{\n"); + for (colors.items) |c| { + try writer.print(" .{{ .r = {}, .g = {}, .b = {} }},\n", .{ c.r, c.g, c.b }); + } + try writer.writeAll(" };\n"); + + try writer.print(" pub const indices = PackedIntSlice(u{}).init(@constCast(data[0..]), data.len * {});\n", .{ args.bits, N }); + try writer.writeAll(" const data = [_]u8{\n"); + for (packed_data, 0..) |index, i| { + if (i % 32 == 0) try writer.writeAll(" "); + try writer.print("{}, ", .{index}); + if ((i + 1) % 32 == 0) try writer.writeAll("\n"); + } + try writer.writeAll(" };\n"); + + try writer.writeAll("};\n\n"); + } +} + +pub const Color = packed struct(u16) { + b: u5, + g: u6, + r: u5, + + fn eql(self: Color, other: Color) bool { + return @as(u16, @bitCast(self)) == @as(u16, @bitCast(other)); + } +}; + +fn getIndex(colors: *std.ArrayList(Color), color: Color) !usize { + for (colors.items, 0..) |c, i| { + if (c.eql(color)) return i; + } + try colors.append(color); + return colors.items.len - 1; +} diff --git a/showcase/carts/dvd/src/main.zig b/showcase/carts/dvd/src/main.zig new file mode 100644 index 0000000..3437b30 --- /dev/null +++ b/showcase/carts/dvd/src/main.zig @@ -0,0 +1,147 @@ +const std = @import("std"); +const cart = @import("cart-api"); +const gfx = @import("gfx"); + +export fn start() void { + // Clear garbage bytes from framebuffer at init since the whole screen is not cleared otherwise. + for (cart.framebuffer[0..]) |*pos| { + pos.* = .{ .r = 0, .b = 0, .g = 0 }; + } +} + +var dvd_hue: f32 = 0; +var dvd_x: isize = 0; +var dvd_y: isize = 0; +var dvd_dx: isize = 2; +var dvd_dy: isize = 1; +var odd_frame = false; + +// These things are super bright at full strength. +const neopixel_brightness = 10.0; + +export fn update() void { + const color = hsv_to_rgb(.{ .h = dvd_hue, .s = 1, .v = 1 }); + drawDvd(gfx.dvd, @intCast(dvd_x), @intCast(dvd_y), color); + dvd_hue += 5; + if (dvd_hue >= 360.0) dvd_hue = 0; + + // The DVD logo gets stuck hitting the places without a fractional angle. + // Offset the dx every other frame to make it look a tiny bit more interesting. + odd_frame = !odd_frame; + dvd_x += if (odd_frame) dvd_dx else @divFloor((dvd_dx * 3), 2); + dvd_y += dvd_dy; + if (dvd_x < 0) { + dvd_dx *= -1; + dvd_x = 0; + } + if (dvd_y < 0) { + dvd_dy *= -1; + dvd_y = 0; + } + if (dvd_x >= cart.screen_width - gfx.dvd.width) { + dvd_dx *= -1; + dvd_x = cart.screen_width - gfx.dvd.width; + } + if (dvd_y >= cart.screen_height - gfx.dvd.height) { + dvd_dy *= -1; + dvd_y = cart.screen_height - gfx.dvd.height; + } + + // Press A to light up the neopixels. + // This was just so we could create a light show in the theater. :> + const np_color: cart.NeopixelColor = if (cart.controls.a) .{ + .g = @intFromFloat(color.g * neopixel_brightness), + .r = @intFromFloat(color.r * neopixel_brightness), + .b = @intFromFloat(color.b * neopixel_brightness), + } else .{ .g = 0, .r = 0, .b = 0 }; + cart.neopixels.* = .{np_color} ** 5; +} + +pub fn drawDvd(sprite: anytype, pos_x: usize, pos_y: usize, color: Rgb) void { + var y: usize = 0; + while (y < sprite.height) : (y += 1) { + var x: usize = 0; + while (x < sprite.width) : (x += 1) { + const dst_x = pos_x + x; + const dst_y = pos_y + y; + const index = y * sprite.width + x; + const src = sprite.colors[sprite.indices.get(index)]; + var dst = &cart.framebuffer[dst_y * cart.screen_width + dst_x]; + dst.r = @intFromFloat(@as(f32, @floatFromInt(src.r)) * color.r); + dst.g = @intFromFloat(@as(f32, @floatFromInt(src.g)) * color.g); + dst.b = @intFromFloat(@as(f32, @floatFromInt(src.b)) * color.b); + cart.framebuffer[dst_y * cart.screen_width + dst_x] = dst.*; + } + } +} + +const Hsv = struct { + h: f32, + s: f32, + v: f32, +}; + +const Rgb = struct { + r: f32, + g: f32, + b: f32, +}; + +fn hsv_to_rgb(in: Hsv) Rgb { + var hh: f32 = undefined; + var p: f32 = undefined; + var q: f32 = undefined; + var t: f32 = undefined; + var ff: f32 = undefined; + var i: i32 = undefined; + var out: Rgb = undefined; + + if (in.s <= 0.0) { + out.r = in.v; + out.g = in.v; + out.b = in.v; + return out; + } + hh = in.h; + if (hh >= 360.0) hh = 0.0; + hh /= 60.0; + i = @intFromFloat(hh); + ff = hh - @as(f32, @floatFromInt(i)); + p = in.v * (1.0 - in.s); + q = in.v * (1.0 - (in.s * ff)); + t = in.v * (1.0 - (in.s * (1.0 - ff))); + + switch (i) { + 0 => { + out.r = in.v; + out.g = t; + out.b = p; + }, + 1 => { + out.r = q; + out.g = in.v; + out.b = p; + }, + 2 => { + out.r = p; + out.g = in.v; + out.b = t; + }, + 3 => { + out.r = p; + out.g = q; + out.b = in.v; + }, + 4 => { + out.r = t; + out.g = p; + out.b = in.v; + }, + else => { + out.r = in.v; + out.g = p; + out.b = q; + }, + } + return out; +}