mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #65 from mitchellh/canvas
wasm: procedurally generated glyphs (i.e. box fonts) for web canvas
This commit is contained in:
@ -35,6 +35,7 @@ fetch(url.href).then(response =>
|
|||||||
group_new,
|
group_new,
|
||||||
group_free,
|
group_free,
|
||||||
group_add_face,
|
group_add_face,
|
||||||
|
group_init_sprite_face,
|
||||||
group_index_for_codepoint,
|
group_index_for_codepoint,
|
||||||
group_render_glyph,
|
group_render_glyph,
|
||||||
group_cache_new,
|
group_cache_new,
|
||||||
@ -81,25 +82,49 @@ fetch(url.href).then(response =>
|
|||||||
//free(font_ptr);
|
//free(font_ptr);
|
||||||
|
|
||||||
// Create our group
|
// Create our group
|
||||||
const group = group_new(72 /* size */);
|
const group = group_new(32 /* size */);
|
||||||
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, 0 /* text */));
|
||||||
group_add_face(group, 0 /* regular */, deferred_face_new(font_name.ptr, font_name.len, 1 /* emoji */));
|
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
|
// Create our group cache
|
||||||
const group_cache = group_cache_new(group);
|
const group_cache = group_cache_new(group);
|
||||||
|
|
||||||
// Render a glyph
|
// 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);
|
const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
|
||||||
group_cache_render_glyph(group_cache, font_idx, i, 0);
|
group_cache_render_glyph(group_cache, font_idx, i, 0);
|
||||||
//face_render_glyph(face, atlas, i);
|
|
||||||
}
|
}
|
||||||
|
for (let i = 0x2580; i <= 0x259f; i++) {
|
||||||
const emoji = ["🐏","🌞","🌚","🍱","💿","🐈","📃","📀","🕡","🙃"];
|
const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
|
||||||
for (let i = 0; i < emoji.length; i++) {
|
group_cache_render_glyph(group_cache, font_idx, i, 0);
|
||||||
const cp = emoji[i].codePointAt(0);
|
}
|
||||||
const font_idx = group_cache_index_for_codepoint(group_cache, cp, 0, -1 /* best choice */);
|
for (let i = 0x2800; i <= 0x28FF; i++) {
|
||||||
group_cache_render_glyph(group_cache, font_idx, cp, 0);
|
const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
|
||||||
|
group_cache_render_glyph(group_cache, font_idx, i, 0);
|
||||||
|
}
|
||||||
|
for (let i = 0x1FB00; i <= 0x1FB3B; i++) {
|
||||||
|
const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
|
||||||
|
group_cache_render_glyph(group_cache, font_idx, i, 0);
|
||||||
|
}
|
||||||
|
for (let i = 0x1FB3C; i <= 0x1FB6B; 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, "橋".codePointAt(0));
|
//face_render_glyph(face, atlas, "橋".codePointAt(0));
|
||||||
|
@ -73,7 +73,7 @@ pub const Fixed = enum(i32) {
|
|||||||
|
|
||||||
pub fn init(v: anytype) Fixed {
|
pub fn init(v: anytype) Fixed {
|
||||||
return switch (@TypeOf(v)) {
|
return switch (@TypeOf(v)) {
|
||||||
comptime_int, u32 => @intToEnum(Fixed, v << 16),
|
comptime_int, i32, u32 => @intToEnum(Fixed, v << 16),
|
||||||
f64 => @intToEnum(Fixed, @floatToInt(i32, v * 65536)),
|
f64 => @intToEnum(Fixed, @floatToInt(i32, v * 65536)),
|
||||||
else => {
|
else => {
|
||||||
@compileLog(@TypeOf(v));
|
@compileLog(@TypeOf(v));
|
||||||
|
@ -272,8 +272,6 @@ pub fn renderGlyph(
|
|||||||
max_height: ?u16,
|
max_height: ?u16,
|
||||||
) !Glyph {
|
) !Glyph {
|
||||||
// Special-case fonts are rendered directly.
|
// Special-case fonts are rendered directly.
|
||||||
// TODO: web_canvas
|
|
||||||
if (options.backend != .web_canvas) {
|
|
||||||
if (index.special()) |sp| switch (sp) {
|
if (index.special()) |sp| switch (sp) {
|
||||||
.sprite => return try self.sprite.?.renderGlyph(
|
.sprite => return try self.sprite.?.renderGlyph(
|
||||||
alloc,
|
alloc,
|
||||||
@ -281,7 +279,6 @@ pub fn renderGlyph(
|
|||||||
glyph_index,
|
glyph_index,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)];
|
const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)];
|
||||||
try face.load(self.lib, self.size);
|
try face.load(self.lib, self.size);
|
||||||
@ -314,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 {
|
export fn group_add_face(self: *Group, style: u16, face: *font.DeferredFace) void {
|
||||||
return self.addFace(alloc, @intToEnum(Style, style), face.*) catch |err| {
|
return self.addFace(alloc, @intToEnum(Style, style), face.*) catch |err| {
|
||||||
log.warn("error adding face to group err={}", .{err});
|
log.warn("error adding face to group err={}", .{err});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
pub const Canvas = @import("sprite/Canvas.zig");
|
pub usingnamespace @import("sprite/canvas.zig");
|
||||||
pub const Face = @import("sprite/Face.zig");
|
pub const Face = @import("sprite/Face.zig");
|
||||||
|
|
||||||
/// Sprites are represented as special codepoints outside of the Unicode
|
/// Sprites are represented as special codepoints outside of the Unicode
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,131 +0,0 @@
|
|||||||
//! This exposes primitives to draw 2D graphics and export the graphic to
|
|
||||||
//! a font atlas.
|
|
||||||
const Canvas = @This();
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const pixman = @import("pixman");
|
|
||||||
const font = @import("../main.zig");
|
|
||||||
|
|
||||||
/// The underlying image.
|
|
||||||
image: *pixman.Image,
|
|
||||||
|
|
||||||
/// The raw data buffer.
|
|
||||||
data: []u32,
|
|
||||||
|
|
||||||
pub const Rect = struct {
|
|
||||||
x: u32,
|
|
||||||
y: u32,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// We only use alpha-channel so a pixel can only be "on" or "off".
|
|
||||||
pub const Color = enum {
|
|
||||||
on,
|
|
||||||
off,
|
|
||||||
|
|
||||||
fn pixmanColor(self: Color) pixman.Color {
|
|
||||||
return switch (self) {
|
|
||||||
.on => .{ .red = 0xFFFF, .green = 0xFFFF, .blue = 0xFFFF, .alpha = 0xFFFF },
|
|
||||||
.off => .{ .red = 0, .green = 0, .blue = 0, .alpha = 0 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = @intCast(usize, stride * @intCast(c_int, height));
|
|
||||||
|
|
||||||
// Allocate our buffer. pixman uses []u32 so we divide our length
|
|
||||||
// by 4 since u32 / u8 = 4.
|
|
||||||
var data = try alloc.alloc(u32, len / 4);
|
|
||||||
errdefer alloc.free(data);
|
|
||||||
std.mem.set(u32, data, 0);
|
|
||||||
|
|
||||||
// Create the image we'll draw to
|
|
||||||
const img = try pixman.Image.createBitsNoClear(
|
|
||||||
format,
|
|
||||||
@intCast(c_int, width),
|
|
||||||
@intCast(c_int, 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 == .greyscale);
|
|
||||||
|
|
||||||
const width = @intCast(u32, self.image.getWidth());
|
|
||||||
const height = @intCast(u32, self.image.getHeight());
|
|
||||||
const region = try atlas.reserve(alloc, width, height);
|
|
||||||
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 = @alignCast(
|
|
||||||
@alignOf(u8),
|
|
||||||
@ptrCast([*]u8, 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: {
|
|
||||||
var 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) {
|
|
||||||
std.mem.copy(u8, dst_ptr, src_ptr[0 .. width * depth]);
|
|
||||||
dst_ptr = dst_ptr[width * depth ..];
|
|
||||||
src_ptr += @intCast(usize, 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 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(i32, v.x),
|
|
||||||
.y1 = @intCast(i32, v.y),
|
|
||||||
.x2 = @intCast(i32, v.x + v.width),
|
|
||||||
.y2 = @intCast(i32, v.y + v.height),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
self.image.fillBoxes(.src, color.pixmanColor(), boxes) catch {};
|
|
||||||
}
|
|
457
src/font/sprite/canvas.zig
Normal file
457
src/font/sprite/canvas.zig
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
//! 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(u32, br_x - tl_x),
|
||||||
|
.height = @intCast(u32, 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 = @intToFloat(f32, std.math.maxInt(u8));
|
||||||
|
const max_u16 = @intToFloat(f32, std.math.maxInt(u16));
|
||||||
|
const unscaled = @intToFloat(f32, @enumToInt(self));
|
||||||
|
const scaled = @floatToInt(u16, (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})", .{
|
||||||
|
@intToFloat(f32, @enumToInt(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 rect(self: *WebCanvasImpl, v: Rect, color: Color) void {
|
||||||
|
const ctx = self.context(color) catch return;
|
||||||
|
defer ctx.deinit();
|
||||||
|
ctx.call(void, "fillRect", .{
|
||||||
|
@intCast(u32, v.x),
|
||||||
|
@intCast(u32, 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 == .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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = @intCast(usize, stride * @intCast(c_int, height));
|
||||||
|
|
||||||
|
// Allocate our buffer. pixman uses []u32 so we divide our length
|
||||||
|
// by 4 since u32 / u8 = 4.
|
||||||
|
var data = try alloc.alloc(u32, len / 4);
|
||||||
|
errdefer alloc.free(data);
|
||||||
|
std.mem.set(u32, data, 0);
|
||||||
|
|
||||||
|
// Create the image we'll draw to
|
||||||
|
const img = try pixman.Image.createBitsNoClear(
|
||||||
|
format,
|
||||||
|
@intCast(c_int, width),
|
||||||
|
@intCast(c_int, 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 == .greyscale);
|
||||||
|
|
||||||
|
const width = @intCast(u32, self.image.getWidth());
|
||||||
|
const height = @intCast(u32, self.image.getHeight());
|
||||||
|
const region = try atlas.reserve(alloc, width, height);
|
||||||
|
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 = @alignCast(
|
||||||
|
@alignOf(u8),
|
||||||
|
@ptrCast([*]u8, 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: {
|
||||||
|
var 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) {
|
||||||
|
std.mem.copy(u8, dst_ptr, src_ptr[0 .. width * depth]);
|
||||||
|
dst_ptr = dst_ptr[width * depth ..];
|
||||||
|
src_ptr += @intCast(usize, 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 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(i32, v.x),
|
||||||
|
.y1 = @intCast(i32, v.y),
|
||||||
|
.x2 = @intCast(i32, v.x + @intCast(i32, v.width)),
|
||||||
|
.y2 = @intCast(i32, v.y + @intCast(i32, v.height)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(i16, dest.x),
|
||||||
|
@intCast(i16, dest.y),
|
||||||
|
@intCast(u16, dest.width),
|
||||||
|
@intCast(u16, dest.height),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -78,7 +78,7 @@ const Draw = struct {
|
|||||||
fn drawSingle(self: Draw, canvas: *font.sprite.Canvas) void {
|
fn drawSingle(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = self.pos,
|
.y = @intCast(i32, self.pos),
|
||||||
.width = self.width,
|
.width = self.width,
|
||||||
.height = self.thickness,
|
.height = self.thickness,
|
||||||
}, .on);
|
}, .on);
|
||||||
@ -88,14 +88,14 @@ const Draw = struct {
|
|||||||
fn drawDouble(self: Draw, canvas: *font.sprite.Canvas) void {
|
fn drawDouble(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = self.pos,
|
.y = @intCast(i32, self.pos),
|
||||||
.width = self.width,
|
.width = self.width,
|
||||||
.height = self.thickness,
|
.height = self.thickness,
|
||||||
}, .on);
|
}, .on);
|
||||||
|
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = self.pos + (self.thickness * 2),
|
.y = @intCast(i32, self.pos + (self.thickness * 2)),
|
||||||
.width = self.width,
|
.width = self.width,
|
||||||
.height = self.thickness,
|
.height = self.thickness,
|
||||||
}, .on);
|
}, .on);
|
||||||
@ -108,8 +108,8 @@ const Draw = struct {
|
|||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < dot_count) : (i += 2) {
|
while (i < dot_count) : (i += 2) {
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = i * dot_width,
|
.x = @intCast(i32, i * dot_width),
|
||||||
.y = self.pos,
|
.y = @intCast(i32, self.pos),
|
||||||
.width = dot_width,
|
.width = dot_width,
|
||||||
.height = self.thickness,
|
.height = self.thickness,
|
||||||
}, .on);
|
}, .on);
|
||||||
@ -123,8 +123,8 @@ const Draw = struct {
|
|||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < dash_count) : (i += 2) {
|
while (i < dash_count) : (i += 2) {
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = i * dash_width,
|
.x = @intCast(i32, i * dash_width),
|
||||||
.y = self.pos,
|
.y = @intCast(i32, self.pos),
|
||||||
.width = dash_width,
|
.width = dash_width,
|
||||||
.height = self.thickness,
|
.height = self.thickness,
|
||||||
}, .on);
|
}, .on);
|
||||||
@ -157,8 +157,8 @@ const Draw = struct {
|
|||||||
while (row < self.thickness) : (row += 1) {
|
while (row < self.thickness) : (row += 1) {
|
||||||
const y1 = @min(row + y + vertical, y_max);
|
const y1 = @min(row + y + vertical, y_max);
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = x,
|
.x = @intCast(i32, x),
|
||||||
.y = y1,
|
.y = @intCast(i32, y1),
|
||||||
.width = 1,
|
.width = 1,
|
||||||
.height = 1,
|
.height = 1,
|
||||||
}, .on);
|
}, .on);
|
||||||
|
Reference in New Issue
Block a user