From 19e326dab67e719376bb764c687ef1738f6cd1a5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 5 Dec 2022 10:54:40 -0800 Subject: [PATCH] font: working on rendering glyphs in canvas --- example/app.ts | 23 +++- example/index.html | 4 +- src/font/face/freetype.zig | 2 +- src/font/face/web_canvas.zig | 215 ++++++++++++++++++++++++++++++----- vendor/zig-js | 2 +- 5 files changed, 209 insertions(+), 37 deletions(-) diff --git a/example/app.ts b/example/app.ts index 8b3528223..65ba6450f 100644 --- a/example/app.ts +++ b/example/app.ts @@ -26,6 +26,10 @@ fetch(url.href).then(response => free, face_new, face_free, + face_render_glyph, + face_debug_canvas, + atlas_new, + atlas_free, } = results.instance.exports; // Give us access to the zjs value for debugging. globalThis.zjs = zjs; @@ -34,16 +38,23 @@ fetch(url.href).then(response => // Initialize our zig-js memory zjs.memory = memory; + // Create our atlas + const atlas = atlas_new(512, 0 /* greyscale */); + // Create some memory for our string const font = new TextEncoder().encode("monospace"); const font_ptr = malloc(font.byteLength); - try { new Uint8Array(memory.buffer, font_ptr).set(font); - // Call whatever example you want: - const face = face_new(font_ptr, font.byteLength, 14); + // Call whatever example you want: + const face = face_new(font_ptr, font.byteLength, 144); + free(font_ptr); + + // Render a glyph + face_render_glyph(face, atlas, "A".codePointAt(0)); + + // Debug our canvas + face_debug_canvas(face); + //face_free(face); - } finally { - free(font_ptr); - } }); diff --git a/example/index.html b/example/index.html index 0866d6256..12b4fb1dd 100644 --- a/example/index.html +++ b/example/index.html @@ -6,6 +6,8 @@ - Open your console, we are just debugging here. +

Open your console, we are just debugging here.

+

The font rendering canvas should show below. This shows a single glyph.

+
diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index eca1da33c..526a980fc 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -113,7 +113,7 @@ pub const Face = struct { /// Returns true if this font is colored. This can be used by callers to /// determine what kind of atlas to pass in. - pub fn hasColor(self: Face) bool { + fn hasColor(self: Face) bool { return self.face.hasColor(); } diff --git a/src/font/face/web_canvas.zig b/src/font/face/web_canvas.zig index 1c552a6f3..4cd7a8de9 100644 --- a/src/font/face/web_canvas.zig +++ b/src/font/face/web_canvas.zig @@ -73,41 +73,120 @@ pub const Face = struct { self.* = undefined; } - /// Calculate the metrics associated with a given face. - fn calcMetrics(self: *Face) !void { - // This will return the same context on subsequent calls so it - // is important to reset it. - const ctx = try self.canvas.call(js.Object, "getContext", .{js.string("2d")}); + /// Resize the font in-place. If this succeeds, the caller is responsible + /// for clearing any glyph caches, font atlas data, etc. + pub fn setSize(self: *Face, size: font.face.DesiredSize) !void { + const old = self.size; + self.size = size; + errdefer self.size = old; + try self.calcMetrics(); + } + + /// Returns the glyph index for the given Unicode code point. For canvas, + /// we support every glyph and the ID is just the codepoint since we don't + /// have access to the underlying tables anyways. We let the browser deal + /// with bad codepoints. + pub fn glyphIndex(self: Face, cp: u32) ?u32 { + _ = self; + return cp; + } + + /// Render a glyph using the glyph index. The rendered glyph is stored in the + /// given texture atlas. + pub fn renderGlyph( + self: Face, + alloc: Allocator, + atlas: *font.Atlas, + glyph_index: u32, + max_height: ?u16, + ) !font.Glyph { + // Encode our glyph into UTF-8 so we can build a JS string out of it. + var utf8: [4]u8 = undefined; + const utf8_len = try std.unicode.utf8Encode(@intCast(u21, glyph_index), &utf8); + const glyph_str = js.string(utf8[0..utf8_len]); + + // Get our drawing context + const measure_ctx = try self.context(); + defer measure_ctx.deinit(); + + // Get the width and height of the render + const metrics = try measure_ctx.call(js.Object, "measureText", .{glyph_str}); + defer metrics.deinit(); + const width: u32 = @floatToInt(u32, @ceil(width: { + // We prefer the bounding box since it is tighter but certain + // text such as emoji do not have a bounding box set so we use + // the full run width instead. + const bounding_right = try metrics.get(f32, "actualBoundingBoxRight"); + if (bounding_right > 0) break :width bounding_right; + break :width try metrics.get(f32, "width"); + })); + + // Height is our ascender + descender for this char + const asc = try metrics.get(f32, "actualBoundingBoxAscent"); + const desc = try metrics.get(f32, "actualBoundingBoxDescent"); + const height = @floatToInt(u32, @ceil(asc + desc)); + + // Resize canvas to match the glyph size exactly + { + try self.canvas.set("width", width); + try self.canvas.set("height", height); + + const width_str = try std.fmt.allocPrint(alloc, "{d}px", .{width}); + defer alloc.free(width_str); + const height_str = try std.fmt.allocPrint(alloc, "{d}px", .{height}); + defer alloc.free(height_str); + + const style = try self.canvas.get(js.Object, "style"); + defer style.deinit(); + try style.set("width", js.string(width_str)); + try style.set("height", js.string(height_str)); + } + + // Reload our context since we resized the canvas + const ctx = try self.context(); defer ctx.deinit(); - // Set our context font - var font_val = try std.fmt.allocPrint( - self.alloc, - "{d}px {s}", - .{ self.size.points, self.font_str }, - ); - defer self.alloc.free(font_val); - try ctx.set("font", js.string(font_val)); + // Set our alignment different since we want it centered exactly + try ctx.set("textBaseline", js.string("top")); - // If the font property didn't change, then the font set didn't work. - // We do this check because it is very easy to put an invalid font - // in and this at least makes it show up in the logs. - { - const check = try ctx.getAlloc(js.String, self.alloc, "font"); - defer self.alloc.free(check); - if (!std.mem.eql(u8, font_val, check)) { - log.warn("canvas font didn't set, fonts may be broken, expected={s} got={s}", .{ - font_val, - check, - }); - } - } + // Draw background + try ctx.set("fillStyle", js.string("transparent")); + try ctx.call(void, "fillRect", .{ + @as(u32, 0), + @as(u32, 0), + width, + height, + }); + + // Draw glyph + try ctx.set("fillStyle", js.string("black")); + try ctx.call(void, "fillText", .{ + glyph_str, + width / 2, + height / 2, + }); + + _ = atlas; + _ = max_height; + return error.Unimplemented; + } + + /// Calculate the metrics associated with a given face. + fn calcMetrics(self: *Face) !void { + const ctx = try self.context(); + defer ctx.deinit(); // Cell width is the width of our M text const cell_width: f32 = cell_width: { const metrics = try ctx.call(js.Object, "measureText", .{js.string("M")}); defer metrics.deinit(); - break :cell_width try metrics.get(f32, "actualBoundingBoxRight"); + + // We prefer the bounding box since it is tighter but certain + // text such as emoji do not have a bounding box set so we use + // the full run width instead. + const bounding_right = try metrics.get(f32, "actualBoundingBoxRight"); + if (bounding_right > 0) break :cell_width bounding_right; + break :cell_width try metrics.get(f32, "width"); }; // To get the cell height we render a high and low character and get @@ -136,7 +215,45 @@ pub const Face = struct { .strikethrough_thickness = underline_thickness, }; - log.debug("metrics font={s} value={}", .{ font_val, self.metrics }); + log.debug("metrics font={s} value={}", .{ self.font_str, self.metrics }); + } + + /// Returns the 2d context configured for drawing + fn context(self: Face) !js.Object { + // This will return the same context on subsequent calls so it + // is important to reset it. + const ctx = try self.canvas.call(js.Object, "getContext", .{js.string("2d")}); + errdefer ctx.deinit(); + + // Clear the canvas + { + const width = try self.canvas.get(f64, "width"); + const height = try self.canvas.get(f64, "height"); + try ctx.call(void, "clearRect", .{ 0, 0, width, height }); + } + + // Set our context font + var font_val = try std.fmt.allocPrint( + self.alloc, + "{d}px {s}", + .{ self.size.points, self.font_str }, + ); + defer self.alloc.free(font_val); + try ctx.set("font", js.string(font_val)); + + // If the font property didn't change, then the font set didn't work. + // We do this check because it is very easy to put an invalid font + // in and this at least makes it show up in the logs. + const check = try ctx.getAlloc(js.String, self.alloc, "font"); + defer self.alloc.free(check); + if (!std.mem.eql(u8, font_val, check)) { + log.warn("canvas font didn't set, fonts may be broken, expected={s} got={s}", .{ + font_val, + check, + }); + } + + return ctx; } }; @@ -165,4 +282,46 @@ pub const Wasm = struct { alloc.destroy(v); } } + + /// Resulting pointer must be freed using the global "free". + export fn face_render_glyph( + face: *Face, + atlas: *font.Atlas, + codepoint: u32, + ) ?*font.Glyph { + return face_render_glyph_(face, atlas, codepoint) catch |err| { + log.warn("error rendering glyph err={}", .{err}); + return null; + }; + } + + export fn face_debug_canvas(face: *Face) void { + face_debug_canvas_(face) catch |err| { + log.warn("error adding debug canvas err={}", .{err}); + }; + } + + fn face_debug_canvas_(face: *Face) !void { + const doc = try js.global.get(js.Object, "document"); + defer doc.deinit(); + + const elem = try doc.call( + ?js.Object, + "getElementById", + .{js.string("face-canvas")}, + ) orelse return error.CanvasContainerNotFound; + defer elem.deinit(); + + try elem.call(void, "append", .{face.canvas}); + } + + fn face_render_glyph_(face: *Face, atlas: *font.Atlas, codepoint: u32) !*font.Glyph { + const glyph = try face.renderGlyph(alloc, atlas, codepoint, null); + + const result = try alloc.create(font.Glyph); + errdefer alloc.destroy(result); + _ = try wasm.toHostOwned(result); + result.* = glyph; + return result; + } }; diff --git a/vendor/zig-js b/vendor/zig-js index a70f5da4f..3aebb1cbe 160000 --- a/vendor/zig-js +++ b/vendor/zig-js @@ -1 +1 @@ -Subproject commit a70f5da4f51b643be47fa39ba697dcd9444204c8 +Subproject commit 3aebb1cbee374025368e223ef23b0c488ba612eb