feat(ttf): Implementar rasterización TTF con antialiasing
- Añadir GlyphPoint, Contour, GlyphOutline para representar contornos - Implementar getGlyphOutline() para parsear tabla glyf - Implementar rasterizeGlyph() con supersampling 2x - Scanline fill con non-zero winding rule - Subdivisión de curvas Bezier cuadráticas - Cache de glyphs por codepoint+size - Alpha blending para antialiasing - Reemplazar drawGlyphPlaceholder con renderizado real El código parsea contornos TTF y los rasteriza con antialiasing. Próximo paso: test visual con AdwaitaSans. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
54626c8edf
commit
69745ba857
1 changed files with 491 additions and 64 deletions
|
|
@ -23,6 +23,250 @@ const Rect = Layout.Rect;
|
||||||
// TTF Data Types
|
// TTF Data Types
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Point on a glyph contour
|
||||||
|
pub const GlyphPoint = struct {
|
||||||
|
x: i16,
|
||||||
|
y: i16,
|
||||||
|
on_curve: bool, // true = on curve, false = control point (quadratic bezier)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A single contour (closed path)
|
||||||
|
pub const Contour = struct {
|
||||||
|
points: []GlyphPoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Glyph outline (all contours)
|
||||||
|
pub const GlyphOutline = struct {
|
||||||
|
contours: []Contour,
|
||||||
|
x_min: i16,
|
||||||
|
y_min: i16,
|
||||||
|
x_max: i16,
|
||||||
|
y_max: i16,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn deinit(self: *GlyphOutline) void {
|
||||||
|
for (self.contours) |contour| {
|
||||||
|
self.allocator.free(contour.points);
|
||||||
|
}
|
||||||
|
self.allocator.free(self.contours);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Rasterized glyph bitmap
|
||||||
|
pub const GlyphBitmap = struct {
|
||||||
|
data: []u8, // Alpha values 0-255
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
bearing_x: i32,
|
||||||
|
bearing_y: i32,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn deinit(self: *GlyphBitmap) void {
|
||||||
|
self.allocator.free(self.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Rasterization
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Edge for scanline rasterization
|
||||||
|
const Edge = struct {
|
||||||
|
x0: f32,
|
||||||
|
y0: f32,
|
||||||
|
x1: f32,
|
||||||
|
y1: f32,
|
||||||
|
direction: i8, // +1 going up, -1 going down
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Rasterize a glyph outline to bitmap with antialiasing
|
||||||
|
pub fn rasterizeGlyph(
|
||||||
|
allocator: Allocator,
|
||||||
|
outline: GlyphOutline,
|
||||||
|
scale: f32,
|
||||||
|
supersample: u8, // 1 = no AA, 2-4 = supersampling level
|
||||||
|
) ?GlyphBitmap {
|
||||||
|
// Calculate scaled bounding box
|
||||||
|
const x_min_f = @as(f32, @floatFromInt(outline.x_min)) * scale;
|
||||||
|
const y_min_f = @as(f32, @floatFromInt(outline.y_min)) * scale;
|
||||||
|
const x_max_f = @as(f32, @floatFromInt(outline.x_max)) * scale;
|
||||||
|
const y_max_f = @as(f32, @floatFromInt(outline.y_max)) * scale;
|
||||||
|
|
||||||
|
const width: u32 = @intFromFloat(@ceil(x_max_f - x_min_f) + 2);
|
||||||
|
const height: u32 = @intFromFloat(@ceil(y_max_f - y_min_f) + 2);
|
||||||
|
|
||||||
|
if (width == 0 or height == 0 or width > 1000 or height > 1000) return null;
|
||||||
|
|
||||||
|
// Allocate bitmap
|
||||||
|
const bitmap_size = width * height;
|
||||||
|
var data = allocator.alloc(u8, bitmap_size) catch return null;
|
||||||
|
@memset(data, 0);
|
||||||
|
|
||||||
|
// Collect all edges from contours
|
||||||
|
var edges_list = std.ArrayList(Edge).init(allocator);
|
||||||
|
defer edges_list.deinit();
|
||||||
|
|
||||||
|
for (outline.contours) |contour| {
|
||||||
|
collectEdgesFromContour(&edges_list, contour.points, scale, x_min_f, y_min_f, supersample) catch return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ss = @as(f32, @floatFromInt(supersample));
|
||||||
|
const ss_sq = @as(f32, @floatFromInt(@as(u32, supersample) * @as(u32, supersample)));
|
||||||
|
|
||||||
|
// Scanline fill with supersampling
|
||||||
|
for (0..height) |py| {
|
||||||
|
for (0..width) |px| {
|
||||||
|
var coverage: u32 = 0;
|
||||||
|
|
||||||
|
// Subsample
|
||||||
|
for (0..supersample) |sy| {
|
||||||
|
for (0..supersample) |sx| {
|
||||||
|
const sample_x = @as(f32, @floatFromInt(px)) + (@as(f32, @floatFromInt(sx)) + 0.5) / ss;
|
||||||
|
const sample_y = @as(f32, @floatFromInt(py)) + (@as(f32, @floatFromInt(sy)) + 0.5) / ss;
|
||||||
|
|
||||||
|
// Count winding number
|
||||||
|
var winding: i32 = 0;
|
||||||
|
for (edges_list.items) |edge| {
|
||||||
|
if (edgeCrossesRay(edge, sample_x, sample_y)) {
|
||||||
|
winding += edge.direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (winding != 0) coverage += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert coverage to alpha
|
||||||
|
const alpha: u8 = @intFromFloat((@as(f32, @floatFromInt(coverage)) / ss_sq) * 255.0);
|
||||||
|
data[py * width + px] = alpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GlyphBitmap{
|
||||||
|
.data = data,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.bearing_x = @intFromFloat(x_min_f),
|
||||||
|
.bearing_y = @intFromFloat(y_max_f), // TTF Y is up, we flip
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect edges from a contour, handling bezier curves
|
||||||
|
fn collectEdgesFromContour(
|
||||||
|
edges: *std.ArrayList(Edge),
|
||||||
|
points: []const GlyphPoint,
|
||||||
|
scale: f32,
|
||||||
|
x_off: f32,
|
||||||
|
y_off: f32,
|
||||||
|
subdivisions: u8,
|
||||||
|
) !void {
|
||||||
|
if (points.len < 2) return;
|
||||||
|
|
||||||
|
const n = points.len;
|
||||||
|
var i: usize = 0;
|
||||||
|
|
||||||
|
while (i < n) {
|
||||||
|
const p0 = points[i];
|
||||||
|
const p1 = points[(i + 1) % n];
|
||||||
|
|
||||||
|
const x0 = @as(f32, @floatFromInt(p0.x)) * scale - x_off;
|
||||||
|
const y0 = @as(f32, @floatFromInt(p0.y)) * scale - y_off;
|
||||||
|
const x1 = @as(f32, @floatFromInt(p1.x)) * scale - x_off;
|
||||||
|
const y1 = @as(f32, @floatFromInt(p1.y)) * scale - y_off;
|
||||||
|
|
||||||
|
if (p0.on_curve and p1.on_curve) {
|
||||||
|
// Straight line
|
||||||
|
try addEdge(edges, x0, y0, x1, y1);
|
||||||
|
i += 1;
|
||||||
|
} else if (p0.on_curve and !p1.on_curve) {
|
||||||
|
// Bezier curve: p0 is on, p1 is control
|
||||||
|
const p2 = points[(i + 2) % n];
|
||||||
|
var x2: f32 = undefined;
|
||||||
|
var y2: f32 = undefined;
|
||||||
|
|
||||||
|
if (p2.on_curve) {
|
||||||
|
x2 = @as(f32, @floatFromInt(p2.x)) * scale - x_off;
|
||||||
|
y2 = @as(f32, @floatFromInt(p2.y)) * scale - y_off;
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
// Two off-curve points: interpolate midpoint
|
||||||
|
x2 = (x1 + @as(f32, @floatFromInt(p2.x)) * scale - x_off) / 2.0;
|
||||||
|
y2 = (y1 + @as(f32, @floatFromInt(p2.y)) * scale - y_off) / 2.0;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subdivide bezier curve
|
||||||
|
try subdivideBezier(edges, x0, y0, x1, y1, x2, y2, subdivisions * 2);
|
||||||
|
} else {
|
||||||
|
// Off-curve start: should have been handled, skip
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subdivide quadratic bezier curve into line segments
|
||||||
|
fn subdivideBezier(
|
||||||
|
edges: *std.ArrayList(Edge),
|
||||||
|
x0: f32,
|
||||||
|
y0: f32,
|
||||||
|
cx: f32,
|
||||||
|
cy: f32,
|
||||||
|
x1: f32,
|
||||||
|
y1: f32,
|
||||||
|
steps: u8,
|
||||||
|
) !void {
|
||||||
|
var prev_x = x0;
|
||||||
|
var prev_y = y0;
|
||||||
|
const step_f = @as(f32, @floatFromInt(steps));
|
||||||
|
|
||||||
|
for (1..steps + 1) |s| {
|
||||||
|
const t = @as(f32, @floatFromInt(s)) / step_f;
|
||||||
|
const t1 = 1.0 - t;
|
||||||
|
|
||||||
|
// Quadratic bezier: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2
|
||||||
|
const curr_x = t1 * t1 * x0 + 2.0 * t1 * t * cx + t * t * x1;
|
||||||
|
const curr_y = t1 * t1 * y0 + 2.0 * t1 * t * cy + t * t * y1;
|
||||||
|
|
||||||
|
try addEdge(edges, prev_x, prev_y, curr_x, curr_y);
|
||||||
|
|
||||||
|
prev_x = curr_x;
|
||||||
|
prev_y = curr_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add edge if it's not horizontal
|
||||||
|
fn addEdge(edges: *std.ArrayList(Edge), x0: f32, y0: f32, x1: f32, y1: f32) !void {
|
||||||
|
// Skip horizontal edges
|
||||||
|
if (@abs(y1 - y0) < 0.001) return;
|
||||||
|
|
||||||
|
// Direction: +1 if going up (y increasing), -1 if going down
|
||||||
|
const direction: i8 = if (y1 > y0) 1 else -1;
|
||||||
|
|
||||||
|
// Always store with y0 < y1
|
||||||
|
if (y0 < y1) {
|
||||||
|
try edges.append(Edge{ .x0 = x0, .y0 = y0, .x1 = x1, .y1 = y1, .direction = direction });
|
||||||
|
} else {
|
||||||
|
try edges.append(Edge{ .x0 = x1, .y0 = y1, .x1 = x0, .y1 = y0, .direction = direction });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if edge crosses horizontal ray from point to the right
|
||||||
|
fn edgeCrossesRay(edge: Edge, px: f32, py: f32) bool {
|
||||||
|
// Ray goes from (px, py) to (+infinity, py)
|
||||||
|
// Edge goes from (x0, y0) to (x1, y1) where y0 < y1
|
||||||
|
|
||||||
|
// Check if ray is within edge's y range
|
||||||
|
if (py < edge.y0 or py >= edge.y1) return false;
|
||||||
|
|
||||||
|
// Calculate x intersection
|
||||||
|
const t = (py - edge.y0) / (edge.y1 - edge.y0);
|
||||||
|
const x_intersect = edge.x0 + t * (edge.x1 - edge.x0);
|
||||||
|
|
||||||
|
// Ray crosses if intersection is to the right of point
|
||||||
|
return x_intersect > px;
|
||||||
|
}
|
||||||
|
|
||||||
/// TTF table directory entry
|
/// TTF table directory entry
|
||||||
const TableEntry = struct {
|
const TableEntry = struct {
|
||||||
tag: [4]u8,
|
tag: [4]u8,
|
||||||
|
|
@ -365,6 +609,160 @@ pub const TtfFont = struct {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get glyph outline (contours with points)
|
||||||
|
pub fn getGlyphOutline(self: Self, glyph_index: u16) ?GlyphOutline {
|
||||||
|
const loc = self.getGlyphLocation(glyph_index) orelse return null;
|
||||||
|
if (loc.length < 10) return null;
|
||||||
|
|
||||||
|
const glyph_data = self.data[self.glyf_offset + loc.offset ..];
|
||||||
|
if (glyph_data.len < loc.length) return null;
|
||||||
|
|
||||||
|
const num_contours: i16 = @bitCast(readU16Big(glyph_data, 0));
|
||||||
|
|
||||||
|
// Compound glyph (negative num_contours) - not supported yet
|
||||||
|
if (num_contours < 0) return null;
|
||||||
|
if (num_contours == 0) return null;
|
||||||
|
|
||||||
|
const x_min: i16 = @bitCast(readU16Big(glyph_data, 2));
|
||||||
|
const y_min: i16 = @bitCast(readU16Big(glyph_data, 4));
|
||||||
|
const x_max: i16 = @bitCast(readU16Big(glyph_data, 6));
|
||||||
|
const y_max: i16 = @bitCast(readU16Big(glyph_data, 8));
|
||||||
|
|
||||||
|
const n_contours: usize = @intCast(num_contours);
|
||||||
|
|
||||||
|
// Read endPtsOfContours
|
||||||
|
var end_pts = self.allocator.alloc(u16, n_contours) catch return null;
|
||||||
|
defer self.allocator.free(end_pts);
|
||||||
|
|
||||||
|
var offset: usize = 10;
|
||||||
|
for (0..n_contours) |i| {
|
||||||
|
if (offset + 2 > glyph_data.len) return null;
|
||||||
|
end_pts[i] = readU16Big(glyph_data, offset);
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const n_points: usize = @as(usize, end_pts[n_contours - 1]) + 1;
|
||||||
|
|
||||||
|
// Skip instructions
|
||||||
|
if (offset + 2 > glyph_data.len) return null;
|
||||||
|
const instruction_length = readU16Big(glyph_data, offset);
|
||||||
|
offset += 2 + instruction_length;
|
||||||
|
|
||||||
|
// Parse flags (with repeat encoding)
|
||||||
|
var flags = self.allocator.alloc(u8, n_points) catch return null;
|
||||||
|
defer self.allocator.free(flags);
|
||||||
|
|
||||||
|
var point_idx: usize = 0;
|
||||||
|
while (point_idx < n_points) {
|
||||||
|
if (offset >= glyph_data.len) return null;
|
||||||
|
const flag = glyph_data[offset];
|
||||||
|
offset += 1;
|
||||||
|
flags[point_idx] = flag;
|
||||||
|
point_idx += 1;
|
||||||
|
|
||||||
|
// Repeat flag
|
||||||
|
if ((flag & 0x08) != 0 and point_idx < n_points) {
|
||||||
|
if (offset >= glyph_data.len) return null;
|
||||||
|
const repeat_count = glyph_data[offset];
|
||||||
|
offset += 1;
|
||||||
|
var r: usize = 0;
|
||||||
|
while (r < repeat_count and point_idx < n_points) : (r += 1) {
|
||||||
|
flags[point_idx] = flag;
|
||||||
|
point_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse X coordinates
|
||||||
|
var x_coords = self.allocator.alloc(i16, n_points) catch return null;
|
||||||
|
defer self.allocator.free(x_coords);
|
||||||
|
|
||||||
|
var x: i16 = 0;
|
||||||
|
for (0..n_points) |i| {
|
||||||
|
const flag = flags[i];
|
||||||
|
const x_short = (flag & 0x02) != 0;
|
||||||
|
const x_same_or_positive = (flag & 0x10) != 0;
|
||||||
|
|
||||||
|
if (x_short) {
|
||||||
|
if (offset >= glyph_data.len) return null;
|
||||||
|
const dx: i16 = @intCast(glyph_data[offset]);
|
||||||
|
offset += 1;
|
||||||
|
x += if (x_same_or_positive) dx else -dx;
|
||||||
|
} else if (!x_same_or_positive) {
|
||||||
|
if (offset + 2 > glyph_data.len) return null;
|
||||||
|
const dx: i16 = @bitCast(readU16Big(glyph_data, offset));
|
||||||
|
offset += 2;
|
||||||
|
x += dx;
|
||||||
|
}
|
||||||
|
// else: x_same_or_positive and !x_short means same as previous
|
||||||
|
x_coords[i] = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Y coordinates
|
||||||
|
var y_coords = self.allocator.alloc(i16, n_points) catch return null;
|
||||||
|
defer self.allocator.free(y_coords);
|
||||||
|
|
||||||
|
var y: i16 = 0;
|
||||||
|
for (0..n_points) |i| {
|
||||||
|
const flag = flags[i];
|
||||||
|
const y_short = (flag & 0x04) != 0;
|
||||||
|
const y_same_or_positive = (flag & 0x20) != 0;
|
||||||
|
|
||||||
|
if (y_short) {
|
||||||
|
if (offset >= glyph_data.len) return null;
|
||||||
|
const dy: i16 = @intCast(glyph_data[offset]);
|
||||||
|
offset += 1;
|
||||||
|
y += if (y_same_or_positive) dy else -dy;
|
||||||
|
} else if (!y_same_or_positive) {
|
||||||
|
if (offset + 2 > glyph_data.len) return null;
|
||||||
|
const dy: i16 = @bitCast(readU16Big(glyph_data, offset));
|
||||||
|
offset += 2;
|
||||||
|
y += dy;
|
||||||
|
}
|
||||||
|
y_coords[i] = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build contours
|
||||||
|
var contours = self.allocator.alloc(Contour, n_contours) catch return null;
|
||||||
|
errdefer self.allocator.free(contours);
|
||||||
|
|
||||||
|
var start_pt: usize = 0;
|
||||||
|
for (0..n_contours) |c| {
|
||||||
|
const end_pt: usize = @as(usize, end_pts[c]) + 1;
|
||||||
|
const contour_len = end_pt - start_pt;
|
||||||
|
|
||||||
|
var points = self.allocator.alloc(GlyphPoint, contour_len) catch {
|
||||||
|
// Free already allocated contours
|
||||||
|
for (0..c) |j| {
|
||||||
|
self.allocator.free(contours[j].points);
|
||||||
|
}
|
||||||
|
self.allocator.free(contours);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (0..contour_len) |i| {
|
||||||
|
const pt_idx = start_pt + i;
|
||||||
|
points[i] = GlyphPoint{
|
||||||
|
.x = x_coords[pt_idx],
|
||||||
|
.y = y_coords[pt_idx],
|
||||||
|
.on_curve = (flags[pt_idx] & 0x01) != 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
contours[c] = Contour{ .points = points };
|
||||||
|
start_pt = end_pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GlyphOutline{
|
||||||
|
.contours = contours,
|
||||||
|
.x_min = x_min,
|
||||||
|
.y_min = y_min,
|
||||||
|
.x_max = x_max,
|
||||||
|
.y_max = y_max,
|
||||||
|
.allocator = self.allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Get horizontal metrics for glyph
|
/// Get horizontal metrics for glyph
|
||||||
pub fn getHMetrics(self: Self, glyph_index: u16) struct { advance: u16, lsb: i16 } {
|
pub fn getHMetrics(self: Self, glyph_index: u16) struct { advance: u16, lsb: i16 } {
|
||||||
if (self.hmtx_offset == 0 or self.hhea_offset == 0) {
|
if (self.hmtx_offset == 0 or self.hhea_offset == 0) {
|
||||||
|
|
@ -438,7 +836,7 @@ pub const TtfFont = struct {
|
||||||
return @intFromFloat(@as(f32, @floatFromInt(self.metrics.descent)) * self.scale);
|
return @intFromFloat(@as(f32, @floatFromInt(self.metrics.descent)) * self.scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw text using TTF font
|
/// Draw text using TTF font with real glyph rasterization
|
||||||
pub fn drawText(
|
pub fn drawText(
|
||||||
self: *Self,
|
self: *Self,
|
||||||
fb: *Framebuffer,
|
fb: *Framebuffer,
|
||||||
|
|
@ -453,90 +851,119 @@ pub const TtfFont = struct {
|
||||||
|
|
||||||
for (text) |c| {
|
for (text) |c| {
|
||||||
if (c == '\n') continue;
|
if (c == '\n') continue;
|
||||||
|
if (c == ' ') {
|
||||||
|
const metrics = self.getGlyphMetrics(c);
|
||||||
|
cx += @intCast(metrics.advance);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get cached glyph or rasterize
|
||||||
|
const cache_key = makeCacheKey(c, self.render_size);
|
||||||
|
|
||||||
|
if (self.glyph_cache.get(cache_key)) |cached| {
|
||||||
|
// Draw cached glyph
|
||||||
|
self.drawGlyphBitmap(fb, cx, baseline_y, cached, color, clip);
|
||||||
|
const metrics = self.getGlyphMetrics(c);
|
||||||
|
cx += @intCast(metrics.advance);
|
||||||
|
} else {
|
||||||
|
// Rasterize and cache
|
||||||
|
const glyph_index = self.getGlyphIndex(c);
|
||||||
|
if (self.getGlyphOutline(glyph_index)) |outline| {
|
||||||
|
defer {
|
||||||
|
var outline_copy = outline;
|
||||||
|
outline_copy.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rasterize with 2x supersampling for antialiasing
|
||||||
|
if (rasterizeGlyph(self.allocator, outline, self.scale, 2)) |bitmap| {
|
||||||
|
const cached_glyph = CachedGlyph{
|
||||||
|
.bitmap = bitmap.data,
|
||||||
|
.metrics = GlyphMetrics{
|
||||||
|
.width = @intCast(bitmap.width),
|
||||||
|
.height = @intCast(bitmap.height),
|
||||||
|
.bearing_x = @intCast(bitmap.bearing_x),
|
||||||
|
.bearing_y = @intCast(bitmap.bearing_y),
|
||||||
|
.advance = self.getGlyphMetrics(c).advance,
|
||||||
|
},
|
||||||
|
.codepoint = c,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.glyph_cache.put(cache_key, cached_glyph) catch {};
|
||||||
|
self.drawGlyphBitmap(fb, cx, baseline_y, cached_glyph, color, clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const metrics = self.getGlyphMetrics(c);
|
const metrics = self.getGlyphMetrics(c);
|
||||||
|
|
||||||
// For now, draw a simple placeholder rectangle
|
|
||||||
// Full glyph rasterization would require bezier curve rendering
|
|
||||||
self.drawGlyphPlaceholder(fb, cx + metrics.bearing_x, baseline_y, c, color, clip);
|
|
||||||
|
|
||||||
cx += @intCast(metrics.advance);
|
cx += @intCast(metrics.advance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw a simple placeholder for glyph (rectangle-based)
|
/// Draw a cached glyph bitmap with alpha blending
|
||||||
fn drawGlyphPlaceholder(
|
fn drawGlyphBitmap(
|
||||||
self: Self,
|
self: Self,
|
||||||
fb: *Framebuffer,
|
fb: *Framebuffer,
|
||||||
x: i32,
|
x: i32,
|
||||||
baseline_y: i32,
|
baseline_y: i32,
|
||||||
char: u8,
|
glyph: CachedGlyph,
|
||||||
color: Color,
|
color: Color,
|
||||||
clip: Rect,
|
clip: Rect,
|
||||||
) void {
|
) void {
|
||||||
// Simple placeholder rendering - draw a rectangle for each character
|
_ = self;
|
||||||
// In a full implementation, this would rasterize the actual glyph outline
|
|
||||||
|
|
||||||
const char_height = self.render_size;
|
// Calculate position: bearing_y is distance from baseline to top
|
||||||
const char_width: u16 = @intFromFloat(@as(f32, @floatFromInt(char_height)) * 0.6);
|
const glyph_x = x + glyph.metrics.bearing_x;
|
||||||
|
const glyph_y = baseline_y - glyph.metrics.bearing_y;
|
||||||
|
|
||||||
const top_y = baseline_y - @as(i32, @intCast(char_height * 3 / 4));
|
const width = glyph.metrics.width;
|
||||||
|
const height = glyph.metrics.height;
|
||||||
|
|
||||||
// Draw character based on simple patterns
|
// Draw each pixel with alpha blending
|
||||||
switch (char) {
|
for (0..height) |py| {
|
||||||
' ' => {}, // Space - nothing to draw
|
for (0..width) |px| {
|
||||||
'.' => {
|
const alpha = glyph.bitmap[py * width + px];
|
||||||
// Dot at baseline
|
if (alpha == 0) continue;
|
||||||
const dot_size: i32 = @max(1, @as(i32, char_height / 8));
|
|
||||||
const dot_x = x + @as(i32, char_width / 2) - dot_size / 2;
|
const screen_x = glyph_x + @as(i32, @intCast(px));
|
||||||
const dot_y = baseline_y - dot_size;
|
const screen_y = glyph_y + @as(i32, @intCast(py));
|
||||||
fb.fillRect(dot_x, dot_y, @intCast(dot_size), @intCast(dot_size), color, clip);
|
|
||||||
},
|
// Clip check
|
||||||
'-' => {
|
if (screen_x < clip.x or screen_x >= clip.x + @as(i32, @intCast(clip.w))) continue;
|
||||||
// Horizontal line in middle
|
if (screen_y < clip.y or screen_y >= clip.y + @as(i32, @intCast(clip.h))) continue;
|
||||||
const line_y = baseline_y - @as(i32, char_height / 3);
|
if (screen_x < 0 or screen_y < 0) continue;
|
||||||
const line_h: u32 = @max(1, char_height / 8);
|
if (screen_x >= @as(i32, @intCast(fb.width)) or screen_y >= @as(i32, @intCast(fb.height))) continue;
|
||||||
fb.fillRect(x + 1, line_y, char_width - 2, line_h, color, clip);
|
|
||||||
},
|
// Alpha blend
|
||||||
'_' => {
|
if (alpha == 255) {
|
||||||
// Underline at baseline
|
fb.setPixel(@intCast(screen_x), @intCast(screen_y), color);
|
||||||
const line_h: u32 = @max(1, char_height / 8);
|
} else {
|
||||||
fb.fillRect(x, baseline_y, char_width, line_h, color, clip);
|
const bg = fb.getPixel(@intCast(screen_x), @intCast(screen_y));
|
||||||
},
|
const blended = blendColors(color, bg, alpha);
|
||||||
'|' => {
|
fb.setPixel(@intCast(screen_x), @intCast(screen_y), blended);
|
||||||
// Vertical line
|
|
||||||
const line_w: u32 = @max(1, char_height / 8);
|
|
||||||
const line_x = x + @as(i32, char_width / 2) - @as(i32, @intCast(line_w / 2));
|
|
||||||
fb.fillRect(line_x, top_y, line_w, char_height, color, clip);
|
|
||||||
},
|
|
||||||
'/' => {
|
|
||||||
// Diagonal (approximate with vertical shifted)
|
|
||||||
const line_w: u32 = @max(1, char_height / 8);
|
|
||||||
var py: i32 = 0;
|
|
||||||
while (py < char_height) : (py += 1) {
|
|
||||||
const px = x + @as(i32, char_width) - (py * @as(i32, char_width)) / @as(i32, char_height);
|
|
||||||
fb.fillRect(px, top_y + py, line_w, 1, color, clip);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
'\\' => {
|
|
||||||
const line_w: u32 = @max(1, char_height / 8);
|
|
||||||
var py: i32 = 0;
|
|
||||||
while (py < char_height) : (py += 1) {
|
|
||||||
const px = x + (py * @as(i32, char_width)) / @as(i32, char_height);
|
|
||||||
fb.fillRect(px, top_y + py, line_w, 1, color, clip);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
else => {
|
|
||||||
// Default: draw a simple block for visibility
|
|
||||||
const inset: i32 = 1;
|
|
||||||
const block_w = if (char_width > 2) char_width - 2 else char_width;
|
|
||||||
const block_h = if (char_height > 2) char_height - 2 else char_height;
|
|
||||||
fb.fillRect(x + inset, top_y + inset, block_w, block_h, color, clip);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Create cache key from codepoint and size
|
||||||
|
fn makeCacheKey(codepoint: u32, size: u16) u64 {
|
||||||
|
return (@as(u64, codepoint) << 16) | @as(u64, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blend foreground color with background using alpha
|
||||||
|
fn blendColors(fg: Color, bg: Color, alpha: u8) Color {
|
||||||
|
const a = @as(u16, alpha);
|
||||||
|
const inv_a = 255 - a;
|
||||||
|
|
||||||
|
return Color{
|
||||||
|
.r = @intCast((@as(u16, fg.r) * a + @as(u16, bg.r) * inv_a) / 255),
|
||||||
|
.g = @intCast((@as(u16, fg.g) * a + @as(u16, bg.g) * inv_a) / 255),
|
||||||
|
.b = @intCast((@as(u16, fg.b) * a + @as(u16, bg.b) * inv_a) / 255),
|
||||||
|
.a = 255,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Helper functions
|
// Helper functions
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue