//! 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, padding_x: u32, padding_y: u32, clip_top: u32 = 0, clip_left: u32 = 0, clip_right: u32 = 0, clip_bottom: u32 = 0, alloc: Allocator, pub fn init( alloc: Allocator, width: u32, height: u32, padding_x: u32, padding_y: u32, ) !Canvas { // Create the surface we'll be using. // We add padding to both sides (hence `2 *`) const sfc = try z2d.Surface.initPixel( .{ .alpha8 = .{ .a = 0 } }, alloc, @intCast(width + 2 * padding_x), @intCast(height + 2 * padding_y), ); errdefer sfc.deinit(alloc); return .{ .sfc = sfc, .padding_x = padding_x, .padding_y = padding_y, .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); self.trim(); const sfc_width: u32 = @intCast(self.sfc.getWidth()); const sfc_height: u32 = @intCast(self.sfc.getHeight()); // Subtract our clip margins from the // width and height to get region size. const region_width = sfc_width -| self.clip_left -| self.clip_right; const region_height = sfc_height -| self.clip_top -| self.clip_bottom; // Allocate our texture atlas region const region = try atlas.reserve(alloc, region_width, region_height); 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 == region_width); assert(region.height == region_height); atlas.setFromLarger( region, buffer, sfc_width, self.clip_left, self.clip_top, ); } return region; } // Adjust clip boundaries to trim off any fully transparent rows or columns. // This circumvents abstractions from z2d so that it can be performant. fn trim(self: *Canvas) void { const width: u32 = @intCast(self.sfc.getWidth()); const height: u32 = @intCast(self.sfc.getHeight()); const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); top: while (self.clip_top < height - self.clip_bottom) { const y = self.clip_top; const x0 = self.clip_left; const x1 = width - self.clip_right; for (buf[y * width ..][x0..x1]) |v| { if (v != 0) break :top; } self.clip_top += 1; } bottom: while (self.clip_bottom < height - self.clip_top) { const y = height - self.clip_bottom -| 1; const x0 = self.clip_left; const x1 = width - self.clip_right; for (buf[y * width ..][x0..x1]) |v| { if (v != 0) break :bottom; } self.clip_bottom += 1; } left: while (self.clip_left < width - self.clip_right) { const x = self.clip_left; const y0 = self.clip_top; const y1 = height - self.clip_bottom; for (y0..y1) |y| { if (buf[y * width + x] != 0) break :left; } self.clip_left += 1; } right: while (self.clip_right < width - self.clip_left) { const x = width - self.clip_right -| 1; const y0 = self.clip_top; const y1 = height - self.clip_bottom; for (y0..y1) |y| { if (buf[y * width + x] != 0) break :right; } self.clip_right += 1; } } /// Only really useful for test purposes, since the clipping region is /// automatically excluded when writing to an atlas with `writeAtlas`. pub fn clearClippingRegions(self: *Canvas) void { const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); const width: usize = @intCast(self.sfc.getWidth()); const height: usize = @intCast(self.sfc.getHeight()); for (0..height) |y| { for (0..self.clip_left) |x| { buf[y * width + x] = 0; } } for (0..height) |y| { for (width - self.clip_right..width) |x| { buf[y * width + x] = 0; } } for (0..self.clip_top) |y| { for (0..width) |x| { buf[y * width + x] = 0; } } for (height - self.clip_bottom..height) |y| { for (0..width) |x| { buf[y * width + x] = 0; } } } /// Return a transformation representing the translation for our padding. pub fn transformation(self: Canvas) z2d.Transformation { return .{ .ax = 1, .by = 0, .cx = 0, .dy = 1, .tx = @as(f64, @floatFromInt(self.padding_x)), .ty = @as(f64, @floatFromInt(self.padding_y)), }; } /// Acquires a z2d drawing context, caller MUST deinit context. pub fn getContext(self: *Canvas) z2d.Context { var ctx = z2d.Context.init(self.alloc, &self.sfc); // Offset by our padding to keep // coordinates relative to the cell. ctx.setTransformation(self.transformation()); return ctx; } /// Draw and fill a single pixel pub fn pixel(self: *Canvas, x: i32, y: i32, color: Color) void { self.sfc.putPixel( x + @as(i32, @intCast(self.padding_x)), y + @as(i32, @intCast(self.padding_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(i32), color: Color) void { var y = v.y; while (y < v.y + v.height) : (y += 1) { var x = v.x; while (x < v.x + v.width) : (x += 1) { self.pixel( @intCast(x), @intCast(y), color, ); } } } /// Convenience wrapper for `Canvas.rect` pub fn box( self: *Canvas, x0: i32, y0: i32, x1: i32, y1: i32, color: Color, ) void { self.rect((Box(i32){ .p0 = .{ .x = x0, .y = y0 }, .p1 = .{ .x = x1, .y = y1 }, }).rect(), color); } /// Draw and fill a quad. pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void { var path = self.staticPath(6); // nodes.len = 0 path.moveTo(q.p0.x, q.p0.y); // +1, nodes.len = 1 path.lineTo(q.p1.x, q.p1.y); // +1, nodes.len = 2 path.lineTo(q.p2.x, q.p2.y); // +1, nodes.len = 3 path.lineTo(q.p3.x, q.p3.y); // +1, nodes.len = 4 path.close(); // +2, nodes.len = 6 try self.fillPath(path.wrapped_path, .{}, color); } /// Draw and fill a triangle. pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void { var path = self.staticPath(5); // nodes.len = 0 path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1 path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2 path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3 path.close(); // +2, nodes.len = 5 try self.fillPath(path.wrapped_path, .{}, color); } /// Stroke a line. pub fn line( self: *Canvas, l: Line(f64), thickness: f64, color: Color, ) !void { var path = self.staticPath(2); // nodes.len = 0 path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1 path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2 try self.strokePath( path.wrapped_path, .{ .line_cap_mode = .butt, .line_width = thickness, }, color, ); } /// Create a static path of the provided len and initialize it. /// Use this function instead of making the path manually since /// it ensures that the transform is applied. pub inline fn staticPath( self: *Canvas, comptime len: usize, ) z2d.StaticPath(len) { var path: z2d.StaticPath(len) = .{}; path.init(); path.wrapped_path.transformation = self.transformation(); return path; } /// Stroke a z2d path. pub fn strokePath( self: *Canvas, path: z2d.Path, opts: z2d.painter.StrokeOpts, color: Color, ) z2d.painter.StrokeError!void { try z2d.painter.stroke( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, path.nodes.items, opts, ); } /// Do an inner stroke on a z2d path, right now this involves a pretty /// heavy workaround that uses two extra surfaces; in the future, z2d /// should add inner and outer strokes natively. pub fn innerStrokePath( self: *Canvas, path: z2d.Path, opts: z2d.painter.StrokeOpts, color: Color, ) (z2d.painter.StrokeError || z2d.painter.FillError)!void { // On one surface we fill the shape, this will be a mask we // multiply with the double-width stroke so that only the // part inside is used. var fill_sfc: z2d.Surface = try .init( .image_surface_alpha8, self.alloc, self.sfc.getWidth(), self.sfc.getHeight(), ); defer fill_sfc.deinit(self.alloc); // On the other we'll do the double width stroke. var stroke_sfc: z2d.Surface = try .init( .image_surface_alpha8, self.alloc, self.sfc.getWidth(), self.sfc.getHeight(), ); defer stroke_sfc.deinit(self.alloc); // Make a closed version of the path for our fill, so // that we can support open paths for inner stroke. var closed_path = path; closed_path.nodes = try path.nodes.clone(self.alloc); defer closed_path.deinit(self.alloc); try closed_path.close(self.alloc); // Fill the shape in white to the fill surface, we use // white because this is a mask that we'll multiply with // the stroke, we want everything inside to be the stroke // color. try z2d.painter.fill( self.alloc, &fill_sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = 255 } }, } }, closed_path.nodes.items, .{}, ); // Stroke the shape with double the desired width. var mut_opts = opts; mut_opts.line_width *= 2; try z2d.painter.stroke( self.alloc, &stroke_sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, path.nodes.items, mut_opts, ); // We multiply the stroke sfc on to the fill surface. // The z2d composite operation doesn't seem to work for // this with alpha8 surfaces, so we have to do it manually. for ( std.mem.sliceAsBytes(fill_sfc.image_surface_alpha8.buf), std.mem.sliceAsBytes(stroke_sfc.image_surface_alpha8.buf), ) |*d, s| { d.* = @intFromFloat(@round( 255.0 * (@as(f64, @floatFromInt(s)) / 255.0) * (@as(f64, @floatFromInt(d.*)) / 255.0), )); } // Then we composite the result on to the main surface. self.sfc.composite(&fill_sfc, .src_over, 0, 0, .{}); } /// Fill a z2d path. pub fn fillPath( self: *Canvas, path: z2d.Path, opts: z2d.painter.FillOpts, color: Color, ) z2d.painter.FillError!void { try z2d.painter.fill( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, path.nodes.items, opts, ); } /// Invert all pixels on the canvas. pub fn invert(self: *Canvas) void { for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| { v.* = 255 - v.*; } } /// Mirror the canvas horizontally. pub fn flipHorizontal(self: *Canvas) Allocator.Error!void { const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); const clone = try self.alloc.dupe(u8, buf); defer self.alloc.free(clone); const width: usize = @intCast(self.sfc.getWidth()); const height: usize = @intCast(self.sfc.getHeight()); for (0..height) |y| { for (0..width) |x| { buf[y * width + x] = clone[y * width + width - x - 1]; } } std.mem.swap(u32, &self.clip_left, &self.clip_right); } /// Mirror the canvas vertically. pub fn flipVertical(self: *Canvas) Allocator.Error!void { const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); const clone = try self.alloc.dupe(u8, buf); defer self.alloc.free(clone); const width: usize = @intCast(self.sfc.getWidth()); const height: usize = @intCast(self.sfc.getHeight()); for (0..height) |y| { for (0..width) |x| { buf[y * width + x] = clone[(height - y - 1) * width + x]; } } std.mem.swap(u32, &self.clip_top, &self.clip_bottom); } };