From bf054e5b44606394dc36251f18b7ef8a310515d9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Dec 2022 22:01:13 -0800 Subject: [PATCH] font: web canvas sprite font can write to atlas --- example/app.ts | 26 ++++++++---- src/font/Group.zig | 23 +++++++++++ src/font/sprite/canvas.zig | 85 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 122 insertions(+), 12 deletions(-) diff --git a/example/app.ts b/example/app.ts index 9f03d2586..8e0778188 100644 --- a/example/app.ts +++ b/example/app.ts @@ -35,6 +35,7 @@ fetch(url.href).then(response => group_new, group_free, group_add_face, + group_init_sprite_face, group_index_for_codepoint, group_render_glyph, group_cache_new, @@ -85,23 +86,32 @@ fetch(url.href).then(response => group_add_face(group, 0 /* regular */, deferred_face_new(font_name.ptr, font_name.len, 0 /* text */)); group_add_face(group, 0 /* regular */, deferred_face_new(font_name.ptr, font_name.len, 1 /* emoji */)); + // Initialize our sprite font, without this we just use the browser. + group_init_sprite_face(group); + // Create our group cache const group_cache = group_cache_new(group); // Render a glyph - for (let i = 33; i <= 126; i++) { + // for (let i = 33; i <= 126; i++) { + // const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1); + // group_cache_render_glyph(group_cache, font_idx, i, 0); + // //face_render_glyph(face, atlas, i); + // } + // + // const emoji = ["🐏","🌞","🌚","🍱","💿","🐈","📃","📀","🕡","🙃"]; + // for (let i = 0; i < emoji.length; i++) { + // const cp = emoji[i].codePointAt(0); + // const font_idx = group_cache_index_for_codepoint(group_cache, cp, 0, -1 /* best choice */); + // group_cache_render_glyph(group_cache, font_idx, cp, 0); + // } + + for (let i = 0x2500; i <= 0x257F; i++) { const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1); group_cache_render_glyph(group_cache, font_idx, i, 0); //face_render_glyph(face, atlas, i); } - const emoji = ["🐏","🌞","🌚","🍱","💿","🐈","📃","📀","🕡","🙃"]; - for (let i = 0; i < emoji.length; i++) { - const cp = emoji[i].codePointAt(0); - const font_idx = group_cache_index_for_codepoint(group_cache, cp, 0, -1 /* best choice */); - group_cache_render_glyph(group_cache, font_idx, cp, 0); - } - //face_render_glyph(face, atlas, "橋".codePointAt(0)); //face_render_glyph(face, atlas, "p".codePointAt(0)); diff --git a/src/font/Group.zig b/src/font/Group.zig index 71e99efdf..5e72ca944 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -311,6 +311,29 @@ pub const Wasm = struct { } } + export fn group_init_sprite_face(self: *Group) void { + return group_init_sprite_face_(self) catch |err| { + log.warn("error initializing sprite face err={}", .{err}); + return; + }; + } + + fn group_init_sprite_face_(self: *Group) !void { + const metrics = metrics: { + const index = self.indexForCodepoint('M', .regular, .text).?; + const face = try self.faceFromIndex(index); + break :metrics face.metrics; + }; + + // Set details for our sprite font + self.sprite = font.sprite.Face{ + .width = @floatToInt(u32, metrics.cell_width), + .height = @floatToInt(u32, metrics.cell_height), + .thickness = 2, + .underline_position = @floatToInt(u32, metrics.underline_position), + }; + } + export fn group_add_face(self: *Group, style: u16, face: *font.DeferredFace) void { return self.addFace(alloc, @intToEnum(Style, style), face.*) catch |err| { log.warn("error adding face to group err={}", .{err}); diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index 8499eb038..932f8e4dc 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -104,6 +104,10 @@ 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; @@ -119,6 +123,8 @@ const WebCanvasImpl = struct { return WebCanvasImpl{ .canvas = canvas, + .width = width, + .height = height, }; } @@ -177,10 +183,81 @@ const WebCanvasImpl = struct { } pub fn writeAtlas(self: *WebCanvasImpl, alloc: Allocator, atlas: *font.Atlas) !font.Atlas.Region { - _ = self; - _ = alloc; - _ = atlas; - return error.Unimplemented; + assert(atlas.format == .greyscale); + + // 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"); + var bitmap = try alloc.alloc(u8, @intCast(usize, 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; } };