//! 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, _, }; /// This is a managed struct, it keeps a reference to the allocator that is /// used to initialize it, and the same allocator is used for any further /// necessary allocations when drawing. 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), ); errdefer sfc.deinit(alloc); return .{ .sfc = sfc, .alloc = alloc }; } pub fn deinit(self: *Canvas) void { self.sfc.deinit(self.alloc); 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; } /// Acquires a z2d drawing context, caller MUST deinit context. pub fn getContext(self: *Canvas) Allocator.Error!z2d.Context { return try z2d.Context.init(self.alloc, &self.sfc); } /// 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) } }, ); } /// 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 path: z2d.StaticPath(6) = .{}; path.init(); path.moveTo(q.p0.x, q.p0.y); path.lineTo(q.p1.x, q.p1.y); path.lineTo(q.p2.x, q.p2.y); path.lineTo(q.p3.x, q.p3.y); path.close(); try z2d.painter.fill( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, &path.nodes, .{}, ); } /// Draw and fill a triangle. pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void { var path: z2d.StaticPath(5) = .{}; path.init(); path.moveTo(t.p0.x, t.p0.y); path.lineTo(t.p1.x, t.p1.y); path.lineTo(t.p2.x, t.p2.y); path.close(); try z2d.painter.fill( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, &path.nodes, .{}, ); } pub fn triangle_outline(self: *Canvas, t: Triangle(f64), thickness: f64, color: Color) !void { var path: z2d.StaticPath(5) = .{}; path.init(); path.moveTo(t.p0.x, t.p0.y); path.lineTo(t.p1.x, t.p1.y); path.lineTo(t.p2.x, t.p2.y); try z2d.painter.stroke( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, &path.nodes, .{ .line_cap_mode = .round, .line_width = thickness, }, ); } /// Stroke a line. pub fn line(self: *Canvas, l: Line(f64), thickness: f64, color: Color) !void { var path: z2d.StaticPath(3) = .{}; path.init(); path.moveTo(l.p0.x, l.p0.y); path.lineTo(l.p1.x, l.p1.y); try z2d.painter.stroke( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, &path.nodes, .{ .line_cap_mode = .round, .line_width = thickness, }, ); } pub fn invert(self: *Canvas) void { for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| { v.* = 255 - v.*; } } };