//! 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 js = @import("zig-js"); const pixman = @import("pixman"); const font = @import("../main.zig"); pub const Point = struct { x: i32, y: i32, }; pub const Line = struct { p1: Point, p2: Point, }; pub const Box = struct { x1: i32, y1: i32, x2: i32, y2: i32, pub fn rect(self: Box) Rect { const tl_x = @min(self.x1, self.x2); const tl_y = @min(self.y1, self.y2); const br_x = @max(self.x1, self.x2); const br_y = @max(self.y1, self.y2); return .{ .x = tl_x, .y = tl_y, .width = @intCast(br_x - tl_x), .height = @intCast(br_y - tl_y), }; } }; pub const Rect = struct { x: i32, y: i32, width: u32, height: u32, }; pub const Triangle = struct { p1: Point, p2: Point, p3: Point, }; pub const Trapezoid = struct { top: i32, bottom: i32, left: Line, right: Line, }; /// We only use alpha-channel so a pixel can only be "on" or "off". pub const Color = enum(u8) { const CSS_BUF_MAX = 24; on = 255, off = 0, _, fn pixmanColor(self: Color) pixman.Color { // pixman uses u16 for color while our color value is u8 so we // scale it up proportionally. const max = @as(f32, @floatFromInt(std.math.maxInt(u8))); const max_u16 = @as(f32, @floatFromInt(std.math.maxInt(u16))); const unscaled = @as(f32, @floatFromInt(@intFromEnum(self))); const scaled = @as(u16, @intFromFloat((unscaled * max_u16) / max)); return .{ .red = 0, .green = 0, .blue = 0, .alpha = scaled }; } fn cssColor(self: Color, buf: []u8) ![]u8 { return try std.fmt.bufPrint(buf, "rgba(0, 0, 0, {:.2})", .{ @as(f32, @floatFromInt(@intFromEnum(self))) / 255, }); } }; /// Composition operations that are supported. pub const CompositionOp = enum { // Note: more can be added here as needed. source_out, fn pixmanOp(self: CompositionOp) pixman.Op { return switch (self) { .source_out => .out, }; } fn jsOp(self: CompositionOp) js.String { return switch (self) { .source_out => js.string("source-out"), }; } }; pub const Canvas = switch (font.options.backend) { .web_canvas => WebCanvasImpl, else => PixmanImpl, }; const WebCanvasImpl = struct { /// The canvas element that is our final image. canvas: js.Object, /// Store the dimensions for easy access later. width: u32, height: u32, pub fn init(alloc: Allocator, width: u32, height: u32) !WebCanvasImpl { _ = alloc; // Create our canvas that we're going to continue to reuse. const doc = try js.global.get(js.Object, "document"); defer doc.deinit(); const canvas = try doc.call(js.Object, "createElement", .{js.string("canvas")}); errdefer canvas.deinit(); // Set our dimensions. try canvas.set("width", width); try canvas.set("height", height); return WebCanvasImpl{ .canvas = canvas, .width = width, .height = height, }; } pub fn deinit(self: *WebCanvasImpl, alloc: Allocator) void { _ = alloc; self.canvas.deinit(); self.* = undefined; } pub fn pixel(self: *WebCanvasImpl, x: u32, y: u32, color: Color) void { const ctx = self.context(color) catch return; defer ctx.deinit(); ctx.call(void, "fillRect", .{ x, y, 1, 1 }) catch return; } pub fn rect(self: *WebCanvasImpl, v: Rect, color: Color) void { const ctx = self.context(color) catch return; defer ctx.deinit(); ctx.call(void, "fillRect", .{ @as(u32, @intCast(v.x)), @as(u32, @intCast(v.y)), v.width, v.height, }) catch return; } pub fn trapezoid(self: *WebCanvasImpl, t: Trapezoid) void { const ctx = self.context(.on) catch return; defer ctx.deinit(); ctx.call(void, "beginPath", .{}) catch return; ctx.call(void, "moveTo", .{ t.left.p1.x, t.left.p1.y }) catch return; ctx.call(void, "lineTo", .{ t.right.p1.x, t.right.p1.y }) catch return; ctx.call(void, "lineTo", .{ t.right.p2.x, t.right.p2.y }) catch return; ctx.call(void, "lineTo", .{ t.left.p2.x, t.left.p2.y }) catch return; ctx.call(void, "fill", .{}) catch return; } pub fn triangle(self: *WebCanvasImpl, t: Triangle, color: Color) void { const ctx = self.context(color) catch return; defer ctx.deinit(); ctx.call(void, "beginPath", .{}) catch return; ctx.call(void, "moveTo", .{ t.p1.x, t.p1.y }) catch return; ctx.call(void, "lineTo", .{ t.p2.x, t.p2.y }) catch return; ctx.call(void, "lineTo", .{ t.p3.x, t.p3.y }) catch return; ctx.call(void, "fill", .{}) catch return; } pub fn composite( self: *WebCanvasImpl, op: CompositionOp, src: *const WebCanvasImpl, dest: Rect, ) void { const ctx = self.context(Color.on) catch return; defer ctx.deinit(); // Set our compositing operation ctx.set("globalCompositeOperation", op.jsOp()) catch return; // Composite ctx.call(void, "drawImage", .{ src.canvas, dest.x, dest.y, dest.width, dest.height, }) catch return; } fn context(self: WebCanvasImpl, fill: ?Color) !js.Object { const ctx = try self.canvas.call(js.Object, "getContext", .{js.string("2d")}); errdefer ctx.deinit(); // Reset our composite operation try ctx.set("globalCompositeOperation", js.string("source-over")); // Set our fill color if (fill) |c| { var buf: [Color.CSS_BUF_MAX]u8 = undefined; const color = try c.cssColor(&buf); try ctx.set("fillStyle", js.string(color)); } return ctx; } pub fn writeAtlas(self: *WebCanvasImpl, alloc: Allocator, atlas: *font.Atlas) !font.Atlas.Region { assert(atlas.format == .grayscale); // Reload our context since we resized the canvas const ctx = try self.context(null); defer ctx.deinit(); // Set our width/height. Set to vars in case we just query the canvas later. const width = self.width; const height = self.height; // Read the image data and get it into a []u8 on our side const bitmap: []u8 = bitmap: { // Read the raw bitmap data and get the "data" value which is a // Uint8ClampedArray. const data = try ctx.call(js.Object, "getImageData", .{ 0, 0, width, height }); defer data.deinit(); const src_array = try data.get(js.Object, "data"); defer src_array.deinit(); // Allocate our local memory to copy the data to. const len = try src_array.get(u32, "length"); const bitmap = try alloc.alloc(u8, @intCast(len)); errdefer alloc.free(bitmap); // Create our target Uint8Array that we can use to copy from src. const mem_array = mem_array: { // Get our runtime memory const mem = try js.runtime.get(js.Object, "memory"); defer mem.deinit(); const buf = try mem.get(js.Object, "buffer"); defer buf.deinit(); // Construct our array to peer into our memory const Uint8Array = try js.global.get(js.Object, "Uint8Array"); defer Uint8Array.deinit(); const mem_array = try Uint8Array.new(.{ buf, bitmap.ptr }); errdefer mem_array.deinit(); break :mem_array mem_array; }; defer mem_array.deinit(); // Copy try mem_array.call(void, "set", .{src_array}); break :bitmap bitmap; }; errdefer alloc.free(bitmap); // Convert the format of the bitmap to A8 since the raw canvas data // is in RGBA. // NOTE(mitchellh): do we need a 1px buffer to avoid artifacts? const bitmap_a8: []u8 = a8: { assert(@mod(bitmap.len, 4) == 0); assert(bitmap.len == width * height * 4); var bitmap_a8 = try alloc.alloc(u8, bitmap.len / 4); errdefer alloc.free(bitmap_a8); var i: usize = 0; while (i < bitmap_a8.len) : (i += 1) { bitmap_a8[i] = bitmap[(i * 4) + 3]; } break :a8 bitmap_a8; }; defer alloc.free(bitmap_a8); // Write the glyph information into the atlas const region = try atlas.reserve(alloc, width, height); if (region.width > 0 and region.height > 0) { assert(region.width == width); assert(region.height == height); atlas.set(region, bitmap_a8); } return region; } }; const PixmanImpl = struct { /// The underlying image. image: *pixman.Image, /// The raw data buffer. data: []u32, pub fn init(alloc: Allocator, width: u32, height: u32) !Canvas { // Determine the config for our image buffer. The images we draw // for boxes are always 8bpp const format: pixman.FormatCode = .a8; const stride = format.strideForWidth(width); const len = @as(usize, @intCast(stride * @as(c_int, @intCast(height)))); // Allocate our buffer. pixman uses []u32 so we divide our length // by 4 since u32 / u8 = 4. const data = try alloc.alloc(u32, len / 4); errdefer alloc.free(data); @memset(data, 0); // Create the image we'll draw to const img = try pixman.Image.createBitsNoClear( format, @intCast(width), @intCast(height), data.ptr, stride, ); errdefer _ = img.unref(); return Canvas{ .image = img, .data = data, }; } pub fn deinit(self: *Canvas, alloc: Allocator) void { alloc.free(self.data); _ = self.image.unref(); self.* = undefined; } /// Write the data in this drawing to the atlas. pub fn writeAtlas(self: *Canvas, alloc: Allocator, atlas: *font.Atlas) !font.Atlas.Region { assert(atlas.format == .grayscale); const width = @as(u32, @intCast(self.image.getWidth())); const height = @as(u32, @intCast(self.image.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 depth = atlas.format.depth(); // Convert our []u32 to []u8 since we use 8bpp formats const stride = self.image.getStride(); const data = @as([*]u8, @ptrCast(self.data.ptr))[0 .. self.data.len * 4]; // We can avoid a buffer copy if our atlas width and bitmap // width match and the bitmap pitch is just the width (meaning // the data is tightly packed). const needs_copy = !(width * depth == stride); // If we need to copy the data, we copy it into a temporary buffer. const buffer = if (needs_copy) buffer: { const temp = try alloc.alloc(u8, width * height * depth); var dst_ptr = temp; var src_ptr = data.ptr; var i: usize = 0; while (i < height) : (i += 1) { @memcpy(dst_ptr[0 .. width * depth], src_ptr[0 .. width * depth]); dst_ptr = dst_ptr[width * depth ..]; src_ptr += @as(usize, @intCast(stride)); } break :buffer temp; } else data[0..(width * height * depth)]; defer if (buffer.ptr != data.ptr) alloc.free(buffer); // 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 { const boxes = &[_]pixman.Box32{ .{ .x1 = @intCast(x), .y1 = @intCast(y), .x2 = @intCast(x + 1), .y2 = @intCast(y + 1), }, }; self.image.fillBoxes(.src, color.pixmanColor(), boxes) catch {}; } /// 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, color: Color) void { const boxes = &[_]pixman.Box32{ .{ .x1 = @intCast(v.x), .y1 = @intCast(v.y), .x2 = @intCast(v.x + @as(i32, @intCast(v.width))), .y2 = @intCast(v.y + @as(i32, @intCast(v.height))), }, }; assert(boxes[0].x1 >= 0); assert(boxes[0].y1 >= 0); assert(boxes[0].x2 <= @as(i32, @intCast(self.image.getWidth()))); assert(boxes[0].y2 <= @as(i32, @intCast(self.image.getHeight()))); self.image.fillBoxes(.src, color.pixmanColor(), boxes) catch {}; } /// Draw and fill a trapezoid. pub fn trapezoid(self: *Canvas, t: Trapezoid) void { self.image.rasterizeTrapezoid(.{ .top = pixman.Fixed.init(t.top), .bottom = pixman.Fixed.init(t.bottom), .left = .{ .p1 = .{ .x = pixman.Fixed.init(t.left.p1.x), .y = pixman.Fixed.init(t.left.p1.y), }, .p2 = .{ .x = pixman.Fixed.init(t.left.p2.x), .y = pixman.Fixed.init(t.left.p2.y), }, }, .right = .{ .p1 = .{ .x = pixman.Fixed.init(t.right.p1.x), .y = pixman.Fixed.init(t.right.p1.y), }, .p2 = .{ .x = pixman.Fixed.init(t.right.p2.x), .y = pixman.Fixed.init(t.right.p2.y), }, }, }, 0, 0); } /// Draw and fill a triangle. pub fn triangle(self: *Canvas, t: Triangle, color: Color) void { const tris = &[_]pixman.Triangle{ .{ .p1 = .{ .x = pixman.Fixed.init(t.p1.x), .y = pixman.Fixed.init(t.p1.y) }, .p2 = .{ .x = pixman.Fixed.init(t.p2.x), .y = pixman.Fixed.init(t.p2.y) }, .p3 = .{ .x = pixman.Fixed.init(t.p3.x), .y = pixman.Fixed.init(t.p3.y) }, }, }; const src = pixman.Image.createSolidFill(color.pixmanColor()) catch return; defer _ = src.unref(); self.image.compositeTriangles(.over, src, .a8, 0, 0, 0, 0, tris); } /// Composite one image on another. pub fn composite(self: *Canvas, op: CompositionOp, src: *const Canvas, dest: Rect) void { self.image.composite( op.pixmanOp(), src.image, null, 0, 0, 0, 0, @intCast(dest.x), @intCast(dest.y), @intCast(dest.width), @intCast(dest.height), ); } };