//! Framebuffer - Pixel buffer for software rendering //! //! A simple 2D array of RGBA pixels. //! The software rasterizer writes to this, then it's blitted to the screen. const std = @import("std"); const Allocator = std.mem.Allocator; const Style = @import("../core/style.zig"); const Color = Style.Color; /// A 2D pixel buffer pub const Framebuffer = struct { allocator: Allocator, pixels: []u32, width: u32, height: u32, const Self = @This(); /// Create a new framebuffer pub fn init(allocator: Allocator, width: u32, height: u32) !Self { const size = @as(usize, width) * @as(usize, height); const pixels = try allocator.alloc(u32, size); @memset(pixels, 0); return .{ .allocator = allocator, .pixels = pixels, .width = width, .height = height, }; } /// Free the framebuffer pub fn deinit(self: Self) void { self.allocator.free(self.pixels); } /// Resize the framebuffer pub fn resize(self: *Self, width: u32, height: u32) !void { if (width == self.width and height == self.height) return; const size = @as(usize, width) * @as(usize, height); const new_pixels = try self.allocator.alloc(u32, size); @memset(new_pixels, 0); self.allocator.free(self.pixels); self.pixels = new_pixels; self.width = width; self.height = height; } /// Clear the entire buffer to a color pub fn clear(self: *Self, color: Color) void { const c = color.toABGR(); @memset(self.pixels, c); } /// Clear a rectangular region to a color (for partial redraw) pub fn clearRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void { const x_start: u32 = if (x < 0) 0 else @intCast(@min(x, @as(i32, @intCast(self.width)))); const y_start: u32 = if (y < 0) 0 else @intCast(@min(y, @as(i32, @intCast(self.height)))); const x_end = @min(x_start + w, self.width); const y_end = @min(y_start + h, self.height); if (x_start >= x_end or y_start >= y_end) return; const c = color.toABGR(); const row_width = x_end - x_start; var py = y_start; while (py < y_end) : (py += 1) { const row_start = py * self.width + x_start; @memset(self.pixels[row_start..][0..row_width], c); } } /// Get pixel at (x, y) pub fn getPixel(self: Self, x: i32, y: i32) ?u32 { if (x < 0 or y < 0) return null; const ux = @as(u32, @intCast(x)); const uy = @as(u32, @intCast(y)); if (ux >= self.width or uy >= self.height) return null; const idx = uy * self.width + ux; return self.pixels[idx]; } /// Set pixel at (x, y) pub fn setPixel(self: *Self, x: i32, y: i32, color: Color) void { if (x < 0 or y < 0) return; const ux = @as(u32, @intCast(x)); const uy = @as(u32, @intCast(y)); if (ux >= self.width or uy >= self.height) return; const idx = uy * self.width + ux; if (color.a == 255) { self.pixels[idx] = color.toABGR(); } else if (color.a > 0) { // Blend with existing pixel const existing = self.pixels[idx]; const bg = Color{ .r = @truncate(existing), .g = @truncate(existing >> 8), .b = @truncate(existing >> 16), .a = @truncate(existing >> 24), }; self.pixels[idx] = color.blend(bg).toABGR(); } } /// Draw a filled rectangle /// Optimized with SIMD-friendly @memset for solid colors (alpha=255) pub fn fillRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void { const x_start = @max(0, x); const y_start = @max(0, y); const x_end = @min(@as(i32, @intCast(self.width)), x + @as(i32, @intCast(w))); const y_end = @min(@as(i32, @intCast(self.height)), y + @as(i32, @intCast(h))); if (x_start >= x_end or y_start >= y_end) return; const c = color.toABGR(); const row_width = @as(u32, @intCast(x_end - x_start)); const ux_start = @as(u32, @intCast(x_start)); // FAST PATH: Solid colors (alpha=255) use @memset which is SIMD-optimized if (color.a == 255) { var py: u32 = @intCast(y_start); const uy_end: u32 = @intCast(y_end); while (py < uy_end) : (py += 1) { const row_start = py * self.width + ux_start; @memset(self.pixels[row_start..][0..row_width], c); } return; } // SLOW PATH: Alpha blending (pixel by pixel) if (color.a > 0) { var py = y_start; while (py < y_end) : (py += 1) { const row_start = @as(u32, @intCast(py)) * self.width; var px = x_start; while (px < x_end) : (px += 1) { const idx = row_start + @as(u32, @intCast(px)); const existing = self.pixels[idx]; const bg = Color{ .r = @truncate(existing), .g = @truncate(existing >> 8), .b = @truncate(existing >> 16), .a = @truncate(existing >> 24), }; self.pixels[idx] = color.blend(bg).toABGR(); } } } } /// Draw a rectangle outline pub fn drawRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void { if (w == 0 or h == 0) return; // Top and bottom self.fillRect(x, y, w, 1, color); self.fillRect(x, y + @as(i32, @intCast(h)) - 1, w, 1, color); // Left and right self.fillRect(x, y + 1, 1, h -| 2, color); self.fillRect(x + @as(i32, @intCast(w)) - 1, y + 1, 1, h -| 2, color); } /// Draw a horizontal line pub fn drawHLine(self: *Self, x: i32, y: i32, w: u32, color: Color) void { self.fillRect(x, y, w, 1, color); } /// Draw a vertical line pub fn drawVLine(self: *Self, x: i32, y: i32, h: u32, color: Color) void { self.fillRect(x, y, 1, h, color); } /// Draw a line (Bresenham's algorithm) pub fn drawLine(self: *Self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) void { var x = x0; var y = y0; const dx = @abs(x1 - x0); const dy = @abs(y1 - y0); const sx: i32 = if (x0 < x1) 1 else -1; const sy: i32 = if (y0 < y1) 1 else -1; var err = @as(i32, @intCast(dx)) - @as(i32, @intCast(dy)); while (true) { self.setPixel(x, y, color); if (x == x1 and y == y1) break; const e2 = err * 2; if (e2 > -@as(i32, @intCast(dy))) { err -= @as(i32, @intCast(dy)); x += sx; } if (e2 < @as(i32, @intCast(dx))) { err += @as(i32, @intCast(dx)); y += sy; } } } /// Get raw pixel data (for blitting to SDL texture) pub fn getData(self: Self) []const u32 { return self.pixels; } /// Get pitch in bytes pub fn getPitch(self: Self) u32 { return self.width * 4; } // ========================================================================= // Rounded Rectangle Drawing (Fancy Mode) // ========================================================================= /// Draw a filled rounded rectangle with optional edge-fade anti-aliasing /// radius: corner radius in pixels /// aa: if true, applies 1-pixel edge fade for smooth borders pub fn fillRoundedRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color, radius: u8, aa: bool) void { if (w == 0 or h == 0) return; // Clamp radius to half the smallest dimension const max_radius = @min(w, h) / 2; const r: u32 = @min(@as(u32, radius), max_radius); if (r == 0) { // No radius, use fast path self.fillRect(x, y, w, h, color); return; } // Calculate bounds const x_start = @max(0, x); const y_start = @max(0, y); const x_end = @min(@as(i32, @intCast(self.width)), x + @as(i32, @intCast(w))); const y_end = @min(@as(i32, @intCast(self.height)), y + @as(i32, @intCast(h))); if (x_start >= x_end or y_start >= y_end) return; // Corner circle centers (relative to rect origin) const r_i32: i32 = @intCast(r); const w_i32: i32 = @intCast(w); const h_i32: i32 = @intCast(h); // Corner centers in screen coordinates const tl_cx = x + r_i32; // top-left const tl_cy = y + r_i32; const tr_cx = x + w_i32 - r_i32; // top-right const tr_cy = y + r_i32; const bl_cx = x + r_i32; // bottom-left const bl_cy = y + h_i32 - r_i32; const br_cx = x + w_i32 - r_i32; // bottom-right const br_cy = y + h_i32 - r_i32; const r_f: f32 = @floatFromInt(r); var py = y_start; while (py < y_end) : (py += 1) { const row_start = @as(u32, @intCast(py)) * self.width; var px = x_start; while (px < x_end) : (px += 1) { // Check which region the pixel is in const in_corner = self.getCornerDistance(px, py, x, y, w_i32, h_i32, tl_cx, tl_cy, tr_cx, tr_cy, bl_cx, bl_cy, br_cx, br_cy, r_f); if (in_corner) |dist| { // In corner region - check distance to arc if (dist <= r_f) { // Inside the arc if (aa and dist > r_f - 1.0) { // Edge fade zone (last pixel) const alpha_f = r_f - dist; const alpha: u8 = @intFromFloat(@min(255.0, @max(0.0, alpha_f * 255.0))); const aa_color = color.withAlpha(@as(u8, @intCast((@as(u16, color.a) * alpha) / 255))); self.blendPixelAt(row_start + @as(u32, @intCast(px)), aa_color); } else { // Fully inside self.blendPixelAt(row_start + @as(u32, @intCast(px)), color); } } // Outside arc - don't draw } else { // Not in corner region - check edge fade for straight edges if (aa) { const edge_dist = self.getEdgeDistance(px, py, x, y, w_i32, h_i32); if (edge_dist < 1.0) { const alpha: u8 = @intFromFloat(@min(255.0, edge_dist * 255.0)); const aa_color = color.withAlpha(@as(u8, @intCast((@as(u16, color.a) * alpha) / 255))); self.blendPixelAt(row_start + @as(u32, @intCast(px)), aa_color); } else { self.blendPixelAt(row_start + @as(u32, @intCast(px)), color); } } else { self.blendPixelAt(row_start + @as(u32, @intCast(px)), color); } } } } } /// Draw a rounded rectangle outline with optional AA pub fn drawRoundedRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color, radius: u8, thickness: u8, aa: bool) void { if (w == 0 or h == 0 or thickness == 0) return; const t: u32 = thickness; // For thin outlines, we can use the difference of two rounded rects // Outer rect self.fillRoundedRect(x, y, w, h, color, radius, aa); // Inner rect (punch out with background) // This is a simplification - proper impl would track background color // For now, we'll draw the outline pixel by pixel // Actually, let's do this properly with a stroke approach const max_radius = @min(w, h) / 2; const r: u32 = @min(@as(u32, radius), max_radius); const inner_r: u32 = if (r > t) r - t else 0; // Draw using edge detection const x_start = @max(0, x); const y_start = @max(0, y); const x_end = @min(@as(i32, @intCast(self.width)), x + @as(i32, @intCast(w))); const y_end = @min(@as(i32, @intCast(self.height)), y + @as(i32, @intCast(h))); if (x_start >= x_end or y_start >= y_end) return; const r_i32: i32 = @intCast(r); const w_i32: i32 = @intCast(w); const h_i32: i32 = @intCast(h); const t_i32: i32 = @intCast(t); const r_f: f32 = @floatFromInt(r); const inner_r_f: f32 = @floatFromInt(inner_r); const t_f: f32 = @floatFromInt(t); // Corner centers const tl_cx = x + r_i32; const tl_cy = y + r_i32; const tr_cx = x + w_i32 - r_i32; const tr_cy = y + r_i32; const bl_cx = x + r_i32; const bl_cy = y + h_i32 - r_i32; const br_cx = x + w_i32 - r_i32; const br_cy = y + h_i32 - r_i32; var py = y_start; while (py < y_end) : (py += 1) { const row_start = @as(u32, @intCast(py)) * self.width; var px = x_start; while (px < x_end) : (px += 1) { // Check if pixel is in the stroke region (between outer and inner bounds) const in_stroke = self.isInStroke(px, py, x, y, w_i32, h_i32, t_i32, tl_cx, tl_cy, tr_cx, tr_cy, bl_cx, bl_cy, br_cx, br_cy, r_f, inner_r_f, t_f, aa); if (in_stroke) |alpha_mult| { if (alpha_mult >= 1.0) { self.blendPixelAt(row_start + @as(u32, @intCast(px)), color); } else if (alpha_mult > 0.0) { const alpha: u8 = @intFromFloat(@min(255.0, alpha_mult * 255.0)); const aa_color = color.withAlpha(@as(u8, @intCast((@as(u16, color.a) * alpha) / 255))); self.blendPixelAt(row_start + @as(u32, @intCast(px)), aa_color); } } } } } // Helper: blend pixel at index with alpha fn blendPixelAt(self: *Self, idx: u32, color: Color) void { if (idx >= self.pixels.len) return; if (color.a == 255) { self.pixels[idx] = color.toABGR(); } else if (color.a > 0) { const existing = self.pixels[idx]; const bg = Color{ .r = @truncate(existing), .g = @truncate(existing >> 8), .b = @truncate(existing >> 16), .a = @truncate(existing >> 24), }; self.pixels[idx] = color.blend(bg).toABGR(); } } // Helper: get distance from pixel to corner arc (null if not in corner region) fn getCornerDistance( self: *Self, px: i32, py: i32, rect_x: i32, rect_y: i32, rect_w: i32, rect_h: i32, tl_cx: i32, tl_cy: i32, tr_cx: i32, tr_cy: i32, bl_cx: i32, bl_cy: i32, br_cx: i32, br_cy: i32, radius: f32, ) ?f32 { _ = self; _ = rect_w; _ = rect_h; // Check if pixel is in a corner region // Top-left corner if (px < tl_cx and py < tl_cy) { const dx: f32 = @floatFromInt(tl_cx - px); const dy: f32 = @floatFromInt(tl_cy - py); return @sqrt(dx * dx + dy * dy); } // Top-right corner if (px > tr_cx and py < tr_cy) { const dx: f32 = @floatFromInt(px - tr_cx); const dy: f32 = @floatFromInt(tr_cy - py); return @sqrt(dx * dx + dy * dy); } // Bottom-left corner if (px < bl_cx and py > bl_cy) { const dx: f32 = @floatFromInt(bl_cx - px); const dy: f32 = @floatFromInt(py - bl_cy); return @sqrt(dx * dx + dy * dy); } // Bottom-right corner if (px > br_cx and py > br_cy) { const dx: f32 = @floatFromInt(px - br_cx); const dy: f32 = @floatFromInt(py - br_cy); return @sqrt(dx * dx + dy * dy); } // Also check if outside rect bounds entirely if (px < rect_x or py < rect_y) return radius + 10.0; // Outside return null; // Not in corner region } // Helper: get minimum distance to edge (for straight edges AA) fn getEdgeDistance(self: *Self, px: i32, py: i32, rect_x: i32, rect_y: i32, rect_w: i32, rect_h: i32) f32 { _ = self; const left: f32 = @floatFromInt(px - rect_x); const right: f32 = @floatFromInt((rect_x + rect_w - 1) - px); const top: f32 = @floatFromInt(py - rect_y); const bottom: f32 = @floatFromInt((rect_y + rect_h - 1) - py); // Return minimum distance to any edge (clamped to positive) return @max(0.0, @min(@min(left, right), @min(top, bottom))) + 1.0; } // Helper: check if pixel is in stroke region for outline fn isInStroke( self: *Self, px: i32, py: i32, rect_x: i32, rect_y: i32, rect_w: i32, rect_h: i32, thickness: i32, tl_cx: i32, tl_cy: i32, tr_cx: i32, tr_cy: i32, bl_cx: i32, bl_cy: i32, br_cx: i32, br_cy: i32, outer_r: f32, inner_r: f32, t_f: f32, aa: bool, ) ?f32 { _ = self; _ = thickness; // Check corners first // Top-left if (px < tl_cx and py < tl_cy) { const dx: f32 = @floatFromInt(tl_cx - px); const dy: f32 = @floatFromInt(tl_cy - py); const dist = @sqrt(dx * dx + dy * dy); if (dist > outer_r) { if (aa and dist < outer_r + 1.0) return outer_r + 1.0 - dist; return null; } if (dist < inner_r) { if (aa and dist > inner_r - 1.0) return dist - (inner_r - 1.0); return null; } return 1.0; } // Top-right if (px > tr_cx and py < tr_cy) { const dx: f32 = @floatFromInt(px - tr_cx); const dy: f32 = @floatFromInt(tr_cy - py); const dist = @sqrt(dx * dx + dy * dy); if (dist > outer_r) { if (aa and dist < outer_r + 1.0) return outer_r + 1.0 - dist; return null; } if (dist < inner_r) { if (aa and dist > inner_r - 1.0) return dist - (inner_r - 1.0); return null; } return 1.0; } // Bottom-left if (px < bl_cx and py > bl_cy) { const dx: f32 = @floatFromInt(bl_cx - px); const dy: f32 = @floatFromInt(py - bl_cy); const dist = @sqrt(dx * dx + dy * dy); if (dist > outer_r) { if (aa and dist < outer_r + 1.0) return outer_r + 1.0 - dist; return null; } if (dist < inner_r) { if (aa and dist > inner_r - 1.0) return dist - (inner_r - 1.0); return null; } return 1.0; } // Bottom-right if (px > br_cx and py > br_cy) { const dx: f32 = @floatFromInt(px - br_cx); const dy: f32 = @floatFromInt(py - br_cy); const dist = @sqrt(dx * dx + dy * dy); if (dist > outer_r) { if (aa and dist < outer_r + 1.0) return outer_r + 1.0 - dist; return null; } if (dist < inner_r) { if (aa and dist > inner_r - 1.0) return dist - (inner_r - 1.0); return null; } return 1.0; } // Straight edges const left_dist: f32 = @floatFromInt(px - rect_x); const right_dist: f32 = @floatFromInt((rect_x + rect_w - 1) - px); const top_dist: f32 = @floatFromInt(py - rect_y); const bottom_dist: f32 = @floatFromInt((rect_y + rect_h - 1) - py); // Check if in stroke region for straight edges const in_left_stroke = left_dist >= 0 and left_dist < t_f; const in_right_stroke = right_dist >= 0 and right_dist < t_f; const in_top_stroke = top_dist >= 0 and top_dist < t_f; const in_bottom_stroke = bottom_dist >= 0 and bottom_dist < t_f; if (in_left_stroke or in_right_stroke or in_top_stroke or in_bottom_stroke) { // AA for outer edge if (aa) { const min_outer = @min(@min(left_dist, right_dist), @min(top_dist, bottom_dist)); if (min_outer < 1.0 and min_outer >= 0) { return min_outer; } } return 1.0; } return null; } }; // ============================================================================= // Tests // ============================================================================= test "Framebuffer basic" { var fb = try Framebuffer.init(std.testing.allocator, 100, 100); defer fb.deinit(); fb.clear(Color.black); fb.setPixel(50, 50, Color.white); const pixel = fb.getPixel(50, 50); try std.testing.expect(pixel != null); } test "Framebuffer fillRect" { var fb = try Framebuffer.init(std.testing.allocator, 100, 100); defer fb.deinit(); fb.clear(Color.black); fb.fillRect(10, 10, 20, 20, Color.red); // Check inside const inside = fb.getPixel(15, 15); try std.testing.expect(inside != null); try std.testing.expectEqual(Color.red.toABGR(), inside.?); // Check outside const outside = fb.getPixel(5, 5); try std.testing.expect(outside != null); try std.testing.expectEqual(Color.black.toABGR(), outside.?); } test "Framebuffer out of bounds" { var fb = try Framebuffer.init(std.testing.allocator, 100, 100); defer fb.deinit(); // These should not crash fb.setPixel(-1, 50, Color.white); fb.setPixel(50, -1, Color.white); fb.setPixel(100, 50, Color.white); fb.setPixel(50, 100, Color.white); try std.testing.expect(fb.getPixel(-1, 50) == null); }