//! This exposes primitives to draw 2D graphics and export the graphic to //! a font atlas. const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const z2d = @import("z2d"); const font = @import("../main.zig"); pub fn Point(comptime T: type) type { return struct { x: T, y: T, }; } pub fn Line(comptime T: type) type { return struct { p0: Point(T), p1: Point(T), }; } pub fn Box(comptime T: type) type { return struct { p0: Point(T), p1: Point(T), pub fn rect(self: Box(T)) Rect(T) { const tl_x = @min(self.p0.x, self.p1.x); const tl_y = @min(self.p0.y, self.p1.y); const br_x = @max(self.p0.x, self.p1.x); const br_y = @max(self.p0.y, self.p1.y); return .{ .x = tl_x, .y = tl_y, .width = br_x - tl_x, .height = br_y - tl_y, }; } }; } pub fn Rect(comptime T: type) type { return struct { x: T, y: T, width: T, height: T, }; } pub fn Triangle(comptime T: type) type { return struct { p0: Point(T), p1: Point(T), p2: Point(T), }; } pub fn Quad(comptime T: type) type { return struct { p0: Point(T), p1: Point(T), p2: Point(T), p3: Point(T), }; } /// We only use alpha-channel so a pixel can only be "on" or "off". pub const Color = enum(u8) { on = 255, off = 0, _, }; pub const Canvas = struct { /// The underlying z2d surface. sfc: z2d.Surface, alloc: Allocator, pub fn init(alloc: Allocator, width: u32, height: u32) !Canvas { // Create the surface we'll be using. const sfc = try z2d.Surface.initPixel( .{ .alpha8 = .{ .a = 0 } }, alloc, @intCast(width), @intCast(height), ); return .{ .sfc = sfc, .alloc = alloc, }; } pub fn deinit(self: *Canvas, alloc: Allocator) void { _ = alloc; self.sfc.deinit(); self.* = undefined; } /// Write the data in this drawing to the atlas. pub fn writeAtlas( self: *Canvas, alloc: Allocator, atlas: *font.Atlas, ) (Allocator.Error || font.Atlas.Error)!font.Atlas.Region { assert(atlas.format == .grayscale); const width = @as(u32, @intCast(self.sfc.getWidth())); const height = @as(u32, @intCast(self.sfc.getHeight())); // Allocate our texture atlas region const region = region: { // We need to add a 1px padding to the font so that we don't // get fuzzy issues when blending textures. const padding = 1; // Get the full padded region var region = try atlas.reserve( alloc, width + (padding * 2), // * 2 because left+right height + (padding * 2), // * 2 because top+bottom ); // Modify the region so that we remove the padding so that // we write to the non-zero location. The data in an Altlas // is always initialized to zero (Atlas.clear) so we don't // need to worry about zero-ing that. region.x += padding; region.y += padding; region.width -= padding * 2; region.height -= padding * 2; break :region region; }; if (region.width > 0 and region.height > 0) { const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf); // Write the glyph information into the atlas assert(region.width == width); assert(region.height == height); atlas.set(region, buffer); } return region; } /// Draw and fill a single pixel pub fn pixel(self: *Canvas, x: u32, y: u32, color: Color) void { self.sfc.putPixel( @intCast(x), @intCast(y), .{ .alpha8 = .{ .a = @intFromEnum(color) } }, ) catch |e| switch (e) { error.OutOfRange => { // If we try to set out of range this will fail. We just silently // ignore it, so that this method (and `rect` which uses it) have // implicit bounds clipping. }, error.InvalidHeight, error.InvalidWidth, error.InvalidPixelFormat, => { std.log.err("unexpected (considered impossible) error err={}", .{e}); unreachable; // This shouldn't be possible. }, }; } /// Draw and fill a rectangle. This is the main primitive for drawing /// lines as well (which are just generally skinny rectangles...) pub fn rect(self: *Canvas, v: Rect(u32), color: Color) void { const x0 = v.x; const x1 = v.x + v.width; const y0 = v.y; const y1 = v.y + v.height; for (y0..y1) |y| { for (x0..x1) |x| { self.pixel( @intCast(x), @intCast(y), color, ); } } } /// Draw and fill a quad. pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void { var ctx: z2d.Context = .{ .surface = self.sfc, .pattern = .{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, }, }, }; var path = z2d.Path.init(self.alloc); defer path.deinit(); try path.moveTo(q.p0.x, q.p0.y); try path.lineTo(q.p1.x, q.p1.y); try path.lineTo(q.p2.x, q.p2.y); try path.lineTo(q.p3.x, q.p3.y); try path.close(); try ctx.fill(self.alloc, path); } /// Draw and fill a triangle. pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void { var ctx: z2d.Context = .{ .surface = self.sfc, .pattern = .{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, }, }, }; var path = z2d.Path.init(self.alloc); defer path.deinit(); try path.moveTo(t.p0.x, t.p0.y); try path.lineTo(t.p1.x, t.p1.y); try path.lineTo(t.p2.x, t.p2.y); try path.close(); try ctx.fill(self.alloc, path); } /// Stroke a line. pub fn line(self: *Canvas, l: Line(f64), thickness: f64, color: Color) !void { var ctx: z2d.Context = .{ .surface = self.sfc, .pattern = .{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, }, }, .line_width = thickness, .line_cap_mode = .round, }; var path = z2d.Path.init(self.alloc); defer path.deinit(); try path.moveTo(l.p0.x, l.p0.y); try path.lineTo(l.p1.x, l.p1.y); try ctx.stroke(self.alloc, path); } pub fn invert(self: *Canvas) void { for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| { v.* = 255 - v.*; } } };