Sistema de rendering dual para zcatgui: Core: - RenderMode enum (simple/fancy) en style.zig - global_render_mode con helpers: isFancy(), setRenderMode() - fillRoundedRect con edge-fade AA en framebuffer.zig (~350 LOC) - Nuevos comandos: rounded_rect, rounded_rect_outline Widgets actualizados: - Button: corner_radius=4, usa roundedRect en fancy mode - Panel: corner_radius=6, show_shadow=true, sombra offset 4px - TextInput: corner_radius=3 - Select: corner_radius=3 - Modal: corner_radius=8, show_shadow=true, sombra offset 6px - Botones y input field del modal también redondeados Técnica edge-fade (de DVUI): - Anti-aliasing por gradiente alfa en bordes - Sin supersampling, mínimo impacto en rendimiento - Bordes suaves sin multisampling +589 líneas, 9 archivos modificados 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
620 lines
22 KiB
Zig
620 lines
22 KiB
Zig
//! 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);
|
|
}
|