#if 0 /* #/ ================================================================ #/ #/ graphics.c #/ #/ ---------------------------------------------------------------- #/ #/ (C) 2025 Mitya Selivanov #/ #/ ================================================================ #/ #/ Self-testing shell script #/ #/ ================================================================ SRC=${0##*./} BIN=${SRC%.*} gcc \ -Wall -Wextra -Werror -pedantic \ -Wno-missing-braces \ -Wno-old-style-declaration \ -Wno-overlength-strings \ -O3 -D NDEBUG \ -D GRAPHICS_TEST_SUITE \ -lX11 -lm -lasound \ -o $BIN $SRC && \ ./$BIN $@ STATUS=$? rm -f $BIN exit $? # */ #endif // ================================================================ #ifndef GRAPHICS_HEADER_GUARD_ #define GRAPHICS_HEADER_GUARD_ #ifdef EVERY_TEST_SUITE #define GRAPHICS_TEST_SUITE #endif #ifdef GRAPHICS_HEADER #define REDUCED_SYSTEM_LAYER_HEADER #endif #include "reduced_system_layer.c" // ================================================================ // // Options // // ================================================================ #ifndef EPSILON #define EPSILON (1e-9) #endif #ifndef ANTIALIASING_SCALE #define ANTIALIASING_SCALE 3 #endif // ================================================================ // // Vector math // // ================================================================ vec3_f32 vec3_from_vec4_f32(vec4_f32 v); vec4_f32 vec4_from_vec3_f32(vec3_f32 v, f32 w); vec3_f32 vec3_f32_lerp (vec3_f32 a, vec3_f32 b, f32 t); vec4_f32 vec4_f32_lerp (vec4_f32 a, vec4_f32 b, f32 t); // ================================================================ typedef struct { i64 width; i64 height; i64 line_size; vec4_f32 *pixels; } Pixel_Buffer; typedef struct { Pixel_Buffer buffer; vec2 position; vec2 scale; b8 quick : 1; // If set, anti-aliasing is skipped. b8 alpha : 1; // If set, the resulting color is: dst * (1 - alpha) + src * alpha. b8 xor_color : 1; // If set, the source color is XORed with the destination color. vec4_f32 color; } Brush; vec3_f32 lab_from_rgb(vec3_f32 rgb); vec3_f32 rgb_from_lab(vec3_f32 lab); vec3_f32 lab_from_lch(vec3_f32 lch); vec3_f32 lch_from_lab(vec3_f32 lab); vec3_f32 rgb_from_lch(vec3_f32 lch); vec3_f32 lch_from_rgb(vec3_f32 rgb); // NOTE: // General rule to work with gamma is removing it, // then doing computations like blending, interpolation, etc, // then adding it back. vec3_f32 rgb_gamma_add (vec3_f32 rgb); vec3_f32 rgb_gamma_remove (vec3_f32 rgb); vec4_f32 rgba_gamma_add (vec4_f32 rgb); vec4_f32 rgba_gamma_remove(vec4_f32 rgb); vec3_f32 rgb_gamma_lerp (vec3_f32 a, vec3_f32 b, f32 t); vec4_f32 rgba_gamma_lerp(vec4_f32 a, vec4_f32 b, f32 t); vec3_f32 rgb_mix (vec3_f32 a, vec3_f32 b, f32 t); vec4_f32 rgba_mix (vec4_f32 a, vec4_f32 b, f32 t); vec3_f32 lch_mix (vec3_f32 a, vec3_f32 b, f32 t); vec3_f32 lch_lerp (vec3_f32 a, vec3_f32 b, f32 t); u32 rgb_u32_from_f32(vec3_f32 color); vec3_f32 rgb_f32_from_u32(u32 color); u32 rgba_u32_from_f32(vec4_f32 color); vec4_f32 rgba_f32_from_u32(u32 color); #define RGB(...) ((Brush) { .color = { __VA_ARGS__, 1.f } }) #define RGBA(...) ((Brush) { .color = { __VA_ARGS__ } }) #define LCH(...) ((Brush) { .color = vec4_from_vec3_f32(rgb_from_lch((vec3_f32) { __VA_ARGS__ }), 1.f) } }) #define LCHA(...) ((Brush) { .color = vec4_from_vec3_f32(rgb_from_lch(vec3_from_vec4_f32((vec4_f32) { __VA_ARGS__ })), ((vec4_f32) { __VA_ARGS__ }).w) } }) b8 rectangle_contains(f64 x0, f64 y0, f64 width, f64 height, f64 px, f64 py); b8 triangle_contains (f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2, f64 px, f64 py); b8 quad_contains (f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2, f64 x3, f64 y3, f64 px, f64 py); b8 ellipse_contains (f64 x0, f64 y0, f64 width, f64 height, f64 px, f64 py); b8 line_contains (f64 x0, f64 y0, f64 x1, f64 y1, f64 width, f64 px, f64 py); Pixel_Buffer frame_pixels (void); Pixel_Buffer sketch_pixels(i64 width, i64 height); Pixel_Buffer subimage (Pixel_Buffer image, i64 x, i64 y, i64 width, i64 height); void put_pixel (Brush brush, i64 x, i64 y); void draw_pixels (Brush brush, f64 x, f64 y, f64 width, f64 height, Pixel_Buffer src); void fill_rectangle (Brush brush, f64 x, f64 y, f64 width, f64 height); void fill_triangle (Brush brush, f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2); void fill_quad (Brush brush, f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2, f64 x3, f64 y3); void fill_ellipse (Brush brush, f64 x, f64 y, f64 width, f64 height); void fill_line (Brush brush, f64 x0, f64 y0, f64 x1, f64 y1, f64 width); void draw_text_area (Brush brush, f64 x, f64 y, f64 width, f64 height, f64 max_scale_x, f64 max_scale_y, i64 num_chars, c32 *text); void draw_text_cursor(Brush brush, f64 x, f64 y, f64 width, f64 height, f64 max_scale_x, f64 max_scale_y, i64 cursor, i64 selection, i64 num_chars, c32 *text); #endif // GRAPHICS_HEADER_GUARD_ // ================================================================ #ifndef GRAPHICS_HEADER #ifndef GRAPHICS_IMPL_GUARD_ #define GRAPHICS_IMPL_GUARD_ static u64 _bitfont[] = { 0xbc0000000000, 0xc00300000, 0x5fd5040093f24fc9, 0xa00a2c2a1a280105, 0xc000415e6f, 0x400000020be0000, 0x1c38a8400000007d, 0x40002043e1020215, 0x408102000000010, 0x9800000000020002, 0xf913e00000033, 0x53200000207c8800, 0x3654880000099, 0x54b800000f840e00, 0xe953c000001a, 0x953e000000674080, 0x1e54b800000f, 0x490000000000240, 0x88a08000000, 0x20a220050a142850, 0x6520800000, 0x912f801eab260be, 0x800034952bf0001f, 0xc850bf0000921427, 0xf00010a54afc0003, 0xd29427800002142b, 0x840007e1023f0000, 0x7d09100000217e, 0x3f000188a08fc000, 0xc30c0cfc00000810, 0x27803f101013f00f, 0xc244bf0000f214, 0x4bf0002f21427800, 0xc254a480006c24, 0x407c00102fc08100, 0xf208080f0000fa0, 0x531007d81c607c0, 0xc208288c031141, 0x83fc00046954b10, 0x180e03000000, 0x41040000000ff04, 0x8102040810000404, 0x2a54600000000101, 0x309123e0000e, 0xc912180000a22447, 0x8000062a54700007, 0xe52a4300000029f0, 0xa0000602043e0001, 0x1d48000002074, 0x1f000003610f8000, 0x13e04f800000010, 0x470000780813e00f, 0x184893e0000e224, 0x23e0001f12243000, 0x82a54100000008, 0x40780000009f0200, 0xe208080e0001f20, 0xa22007981860780, 0x82082888022282, 0x16c200004ca95320, 0x7f000004, 0x408200000086d04, 0x8204, }; enum { CHAR_NUM_BITS_X_ = 6, CHAR_NUM_BITS_Y_ = 7, CHAR_NUM_BITS_ = CHAR_NUM_BITS_X_ * CHAR_NUM_BITS_Y_, }; static f64 min3_(f64 a, f64 b, f64 c) { if (a < b && a < c) return a; if (b < c) return b; return c; } static f64 max3_(f64 a, f64 b, f64 c) { if (a > b && a > c) return a; if (b > c) return b; return c; } static f64 min4_(f64 a, f64 b, f64 c, f64 d) { if (a < b && a < c && a < d) return a; if (b < c && b < d) return b; if (c < d) return c; return d; } static f64 max4_(f64 a, f64 b, f64 c, f64 d) { if (a > b && a > c && a > d) return a; if (b > c && b > d) return b; if (c > d) return c; return d; } static b8 same_sign_(f64 a, f64 b) { if (a >= EPSILON && b <= -EPSILON) return 0; if (a <= -EPSILON && b >= EPSILON) return 0; return 1; } static f64 gamma_(f64 x) { if (x >= 0.0031308) return 1.055 * pow(x, 1.0 / 2.4) - 0.055; return 12.92 * x; } static f64 gamma_re_(f64 x) { if (x >= 0.04045) return pow((x + 0.055) / 1.055, 2.4); return x / 12.92; } static void put_pixel_color_(Brush brush, i64 x, i64 y) { brush.buffer.pixels[y * brush.buffer.line_size + x] = brush.color; } static void put_pixel_alpha_(Brush brush, i64 x, i64 y) { i64 n = y * brush.buffer.line_size + x; vec4_f32 dst = brush.buffer.pixels[n]; f64 a = brush.color.w; brush.buffer.pixels[n] = (vec4_f32) { .x = (f32) (dst.x * (1. - a) + brush.color.x * a), .y = (f32) (dst.y * (1. - a) + brush.color.y * a), .z = (f32) (dst.z * (1. - a) + brush.color.z * a), .w = 1., }; } static void put_pixel_xor_(Brush brush, i64 x, i64 y) { i64 n = y * brush.buffer.line_size + x; vec3_f32 c = rgb_f32_from_u32( rgb_u32_from_f32(vec3_from_vec4_f32(brush.buffer.pixels[n])) ^ rgb_u32_from_f32(vec3_from_vec4_f32(brush.color)) ); brush.color = vec4_from_vec3_f32(c, brush.color.w); if (brush.alpha) put_pixel_alpha_(brush, x, y); else brush.buffer.pixels[n] = brush.color; } static void put_pixel_(Brush brush, i64 x, i64 y) { if (brush.xor_color) put_pixel_xor_(brush, x, y); else if (brush.alpha) put_pixel_alpha_(brush, x, y); else put_pixel_color_(brush, x, y); } f64 transform_x_(Brush brush, f64 x) { return (brush.position.x + x) * brush.scale.x; } f64 transform_y_(Brush brush, f64 y) { return (brush.position.y + y) * brush.scale.y; } void transform_box_(Brush brush, f64 x, f64 y, f64 width, f64 height, f64 *x0, f64 *y0, f64 *x1, f64 *y1) { if (brush.scale.x < 0) { *x1 = transform_x_(brush, x); *x0 = *x1 + width * brush.scale.x; } else { *x0 = transform_x_(brush, x); *x1 = *x0 + width * brush.scale.x; } if (brush.scale.y < 0) { *y1 = transform_y_(brush, y); *y0 = *y1 + height * brush.scale.y; } else { *y0 = transform_y_(brush, y); *y1 = *y0 + height * brush.scale.y; } } Brush antialiasing_brush_(Brush brush) { return (Brush) { .buffer = sketch_pixels(brush.buffer.width * ANTIALIASING_SCALE, brush.buffer.height * ANTIALIASING_SCALE), .position = brush.position, .scale = { .x = brush.scale.x * ANTIALIASING_SCALE, .y = brush.scale.y * ANTIALIASING_SCALE, }, .quick = 1, .alpha = 0, .xor_color = 0, .color = brush.alpha ? brush.color : vec4_from_vec3_f32(vec3_from_vec4_f32(brush.color), 1.f), }; } Brush copy_brush_(Brush brush) { return (Brush) { .buffer = brush.buffer, .position = brush.position, .scale = brush.scale, .alpha = 1, .xor_color = brush.xor_color, }; } void draw_pixels_(Brush brush, f64 x, f64 y, f64 width, f64 height, Pixel_Buffer src) { // FIXME PERF f64 x0, y0, x1, y1; transform_box_(brush, x, y, width, height, &x0, &y0, &x1, &y1); f64 w = x1 - x0; f64 h = y1 - y0; if (w < EPSILON || h < EPSILON) return; i64 i0 = (i64) floor(x0); i64 i1 = (i64) floor(x1); i64 j0 = (i64) floor(y0); i64 j1 = (i64) floor(y1); if (i0 < 0) i0 = 0; if (i1 >= brush.buffer.width) i1 = brush.buffer.width - 1; if (j0 < 0) j0 = 0; if (j1 >= brush.buffer.height) j1 = brush.buffer.height - 1; f64 w_inv = 1. / w; f64 h_inv = 1. / h; if (!brush.quick) for (i64 j = j0; j <= j1; ++j) { i64 src_j0 = (i64) floor(((j - y0) * src.height) * h_inv + .5); i64 src_j1 = (i64) floor(((j + 1 - y0) * src.height) * h_inv + .5); if (src_j0 == src_j1) src_j1 = src_j0 + 1; if (src_j1 <= 0 || src_j0 >= src.height) continue; if (src_j0 < 0) src_j0 = 0; if (src_j1 > src.height) src_j1 = src.height; for (i64 i = i0; i <= i1; ++i) { i64 src_i0 = (i64) floor(((i - x0) * src.width) * w_inv + .5); i64 src_i1 = (i64) floor(((i + 1 - x0) * src.width) * w_inv + .5); if (src_i0 == src_i1) src_i1 = src_i0 + 1; if (src_i1 <= 0 || src_i0 >= src.width) continue; if (src_i0 < 0) src_i0 = 0; if (src_i1 > src.width) src_i1 = src.width; brush.color = (vec4_f32) {0}; i64 n = 0; for (i64 jj = src_j0; jj < src_j1; ++jj) for (i64 ii = src_i0; ii < src_i1; ++ii) { brush.color.x += gamma_re_(src.pixels[jj * src.line_size + ii].x); brush.color.y += gamma_re_(src.pixels[jj * src.line_size + ii].y); brush.color.z += gamma_re_(src.pixels[jj * src.line_size + ii].z); brush.color.w += gamma_re_(src.pixels[jj * src.line_size + ii].w); ++n; } if (n == 0) continue; f32 n_inv = 1.f / n; brush.color.x = gamma_(brush.color.x * n_inv); brush.color.y = gamma_(brush.color.y * n_inv); brush.color.z = gamma_(brush.color.z * n_inv); brush.color.w = gamma_(brush.color.w * n_inv); put_pixel_(brush, i, j); } } else for (i64 j = j0; j <= j1; ++j) { i64 src_j = (i64) floor(((j - y0) * src.height) * h_inv + .5); if (src_j < 0 || src_j >= src.height) continue; for (i64 i = i0; i <= i1; ++i) { i64 src_i = (i64) floor(((i - x0) * src.width) * w_inv + .5); if (src_i < 0 || src_i >= src.width) continue; brush.color = src.pixels[src_j * src.line_size + src_i]; put_pixel_(brush, i, j); } } } void draw_pixels_from_(Brush brush, f64 x, f64 y, f64 width, f64 height, Pixel_Buffer src, i64 src_x, i64 src_y, i64 src_width, i64 src_height) { // NOTE: In the future we may need to implement // pixel buffers with bounding boxes. f64 bx = 0; f64 by = 0; f64 bw = 0; f64 bh = 0; if (src_width > src.width - src_x) { bw += src_width - src.width + src_x; src_width = src.width - src_x; } if (src_height > src.height - src_y) { bh += src_height - src.height + src_y; src_height = src.height - src_y; } if (src_x < 0) { bx -= src_x; bw -= src_x; src_x = 0; } if (src_y < 0) { by -= src_y; bh -= src_y; src_y = 0; } if (src_x + src_width > src.width || src_y + src_height > src.height || src_width <= 0 || src_height <= 0) return; f64 w_inv = 1. / src_width; f64 h_inv = 1. / src_height; bx *= width * w_inv; by *= height * h_inv; bw *= width * w_inv; bh *= height * h_inv; draw_pixels_(brush, x + bx, y + by, width - bw, height - bh, subimage(src, src_x, src_y, src_width, src_height)); } void fill_rectangle_(Brush brush, f64 x, f64 y, f64 width, f64 height) { if (width < EPSILON || height < EPSILON) return; f64 x0, y0, x1, y1; transform_box_(brush, x, y, width, height, &x0, &y0, &x1, &y1); if (!brush.quick) { i64 i0 = (i64) ceil (x0); i64 i1 = (i64) floor(x1); i64 j0 = (i64) ceil (y0); i64 j1 = (i64) floor(y1); i64 i00 = i0; i64 j00 = j0; i64 i10 = i1; i64 j10 = j1; if (i0 < 0) i0 = 0; if (i1 > brush.buffer.width) i1 = brush.buffer.width; if (j0 < 0) j0 = 0; if (j1 > brush.buffer.height) j1 = brush.buffer.height; f64 kx0 = i00 - x0; f64 kx1 = x1 - i10; f64 ky0 = j00 - y0; f64 ky1 = y1 - j10; if (brush.xor_color) for (i64 j = j0; j < j1; ++j) for (i64 i = i0; i < i1; ++i) put_pixel_xor_(brush, i, j); else if (brush.alpha) for (i64 j = j0; j < j1; ++j) for (i64 i = i0; i < i1; ++i) put_pixel_alpha_(brush, i, j); else for (i64 j = j0; j < j1; ++j) for (i64 i = i0; i < i1; ++i) put_pixel_color_(brush, i, j); if (i00 > i10) { kx0 *= kx1; kx1 = 0.; } if (j00 > j10) { ky0 *= ky1; ky1 = 0.; } f64 alpha = brush.alpha ? brush.color.w : 1.; brush.alpha = 1; if (i00 - 1 >= 0 && i00 - 1 < brush.buffer.width) { brush.color.w = gamma_(gamma_re_(alpha) * kx0); for (i64 j = j0; j < j1; ++j) put_pixel_(brush, i00 - 1, j); } if (i10 >= 0 && i10 < brush.buffer.width) { brush.color.w = gamma_(gamma_re_(alpha) * kx1); for (i64 j = j0; j < j1; ++j) put_pixel_(brush, i10, j); } if (j00 - 1 >= 0 && j00 - 1 < brush.buffer.height) { brush.color.w = gamma_(gamma_re_(alpha) * ky0); for (i64 i = i0; i < i1; ++i) put_pixel_(brush, i, j00 - 1); } if (j10 >= 0 && j10 < brush.buffer.height) { brush.color.w = gamma_(gamma_re_(alpha) * ky1); for (i64 i = i0; i < i1; ++i) put_pixel_(brush, i, j10); } if ( i00 - 1 >= 0 && i00 - 1 < brush.buffer.width && j00 - 1 >= 0 && j00 - 1 < brush.buffer.height) { brush.color.w = gamma_(gamma_re_(alpha) * kx0 * ky0); put_pixel_(brush, i00 - 1, j00 - 1); } if ( i10 >= 0 && i10 < brush.buffer.width && j00 - 1 >= 0 && j00 - 1 < brush.buffer.height) { brush.color.w = gamma_(gamma_re_(alpha) * kx1 * ky0); put_pixel_(brush, i10, j00 - 1); } if ( i00 - 1 >= 0 && i00 - 1 < brush.buffer.width && j10 >= 0 && j10 < brush.buffer.height) { brush.color.w = gamma_(gamma_re_(alpha) * kx0 * ky1); put_pixel_(brush, i00 - 1, j10); } if ( i10 >= 0 && i10 < brush.buffer.width && j10 >= 0 && j10 < brush.buffer.height) { brush.color.w = gamma_(gamma_re_(alpha) * kx1 * ky1); put_pixel_(brush, i10, j10); } } else { i64 i0 = (i64) floor(x0); i64 i1 = (i64) ceil (x1); i64 j0 = (i64) floor(y0); i64 j1 = (i64) ceil (y1); if (i0 < 0) i0 = 0; if (i1 > brush.buffer.width) i1 = brush.buffer.width; if (j0 < 0) j0 = 0; if (j1 > brush.buffer.height) j1 = brush.buffer.height; if (brush.xor_color) for (i64 j = j0; j < j1; ++j) for (i64 i = i0; i < i1; ++i) put_pixel_xor_(brush, i, j); else if (brush.alpha) for (i64 j = j0; j < j1; ++j) for (i64 i = i0; i < i1; ++i) put_pixel_alpha_(brush, i, j); else for (i64 j = j0; j < j1; ++j) for (i64 i = i0; i < i1; ++i) put_pixel_color_(brush, i, j); } } void fill_triangle_(Brush brush, f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2) { // FIXME PERF: // Implement better algorithm. x0 = transform_x_(brush, x0); y0 = transform_y_(brush, y0); x1 = transform_x_(brush, x1); y1 = transform_y_(brush, y1); x2 = transform_x_(brush, x2); y2 = transform_y_(brush, y2); i64 i0 = (i64) floor(min3_(x0, x1, x2)); i64 j0 = (i64) floor(min3_(y0, y1, y2)); i64 i1 = (i64) ceil (max3_(x0, x1, x2)); i64 j1 = (i64) ceil (max3_(y0, y1, y2)); if (j0 < 0) j0 = 0; if (j1 >= brush.buffer.height) j1 = brush.buffer.height - 1; for (i64 j = j0; j <= j1; ++j) { i64 left = i1; i64 right = i0; for (i64 i = i0; i <= i1; ++i) if (triangle_contains(x0, y0, x1, y1, x2, y2, (f64) i, (f64) j)) { left = i; break; } for (i64 i = i1; i >= i0; --i) if (triangle_contains(x0, y0, x1, y1, x2, y2, (f64) i, (f64) j)) { right = i; break; } if (left < 0) left = 0; if (right >= brush.buffer.width) right = brush.buffer.width - 1; if (brush.xor_color) for (i64 i = left; i <= right; ++i) put_pixel_xor_(brush, i, j); else if (brush.alpha) for (i64 i = left; i <= right; ++i) put_pixel_alpha_(brush, i, j); else for (i64 i = left; i <= right; ++i) put_pixel_color_(brush, i, j); } } void fill_quad_(Brush brush, f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2, f64 x3, f64 y3) { fill_triangle_(brush, x0, y0, x1, y1, x2, y2); fill_triangle_(brush, x0, y0, x2, y2, x3, y3); } void fill_ellipse_(Brush brush, f64 x, f64 y, f64 width, f64 height) { f64 x0, y0, x1, y1; transform_box_(brush, x, y, width, height, &x0, &y0, &x1, &y1); i64 i0 = (i64) floor(x0); i64 i1 = (i64) ceil (x1); i64 j0 = (i64) floor(y0); i64 j1 = (i64) ceil (y1); if (j0 < 0) j0 = 0; if (j1 >= brush.buffer.height) j1 = brush.buffer.height - 1; for (i64 j = j0; j <= j1; ++j) { i64 left = i1; i64 right = i0; for (i64 i = i0; i <= i1; ++i) if (ellipse_contains(x0, y0, x1 - x0, y1 - y0, (f64) i, (f64) j)) { left = i; right = (i64) ceil(x0 + x1 - left); // symmetry break; } for (i64 i = right; i >= i0; --i) if (ellipse_contains(x0, y0, x1 - x0, y1 - y0, (f64) i, (f64) j)) { right = i; break; } if (left < 0) left = 0; if (right >= brush.buffer.width) right = brush.buffer.width - 1; if (brush.xor_color) for (i64 i = left; i <= right; ++i) put_pixel_xor_(brush, i, j); else if (brush.alpha) for (i64 i = left; i <= right; ++i) put_pixel_alpha_(brush, i, j); else for (i64 i = left; i <= right; ++i) put_pixel_color_(brush, i, j); } } static i64 char_column_offset_(c32 c, i64 column_index) { if (column_index < 0 || column_index >= CHAR_NUM_BITS_X_) return -1; return (c - 32) * CHAR_NUM_BITS_ + column_index * CHAR_NUM_BITS_Y_; } static b8 char_bit_(i64 column_offset, i64 row_index) { if (column_offset < 0 || row_index < 0 || row_index >= CHAR_NUM_BITS_Y_) return 0; i64 bit_index = column_offset + row_index; i64 qword_index = bit_index / 64; if (qword_index < 0 || qword_index >= (i64) (sizeof _bitfont / sizeof *_bitfont)) return 0; u64 mask = 1ull << (bit_index % 64); return !!(_bitfont[qword_index] & mask); } static u64 char_column_convolved_(c32 c, i64 column_index) { if (column_index < 0 || column_index >= CHAR_NUM_BITS_X_) return 0; u64 column = 0; i64 offset = char_column_offset_(c, column_index); for (i64 y = 0; y < CHAR_NUM_BITS_Y_; ++y) if (char_bit_(offset, y)) column |= 3ull << y; return column; } static b8 char_column_empty_(c32 c, i64 column_index) { if (column_index < 0 || column_index >= CHAR_NUM_BITS_X_) return 1; i64 offset = char_column_offset_(c, column_index); for (i64 y = 0; y < CHAR_NUM_BITS_Y_; ++y) if (char_bit_(offset, y)) return 0; return 1; } static i64 char_width_(c32 c) { if (c < 32) return 0; if (c == ' ' || c > 127) return 4; i64 width = 0; for (; width < CHAR_NUM_BITS_X_; ++width) if (char_column_empty_(c, width) && char_column_empty_(c, width + 1)) break; return width; } static i64 char_spacing_(i64 num_chars, c32 *text, i64 index) { if (text == NULL) return 0; if (index < 0 || index + 1 >= num_chars) return 0; u64 a = char_column_convolved_(text[index], char_width_(text[index]) - 1); u64 b = char_column_convolved_(text[index + 1], 0); if (!!(a & b)) return 1; return 0; } static i64 text_cursor_(i64 num_chars, c32 *text) { if (text == NULL) return 0; i64 cursor = 0; for (i64 i = 0; i < num_chars; ++i) { if (text[i] <= ' ') { if (text[i] == '\n') cursor = 0; else if (text[i] == '\b' && i > 0) cursor -= char_width_(text[i - 1]) + char_spacing_(num_chars, text, i - 1); else if (text[i] == '\r') cursor = 0; else cursor += char_width_(' ') + char_spacing_(num_chars, text, i); continue; } cursor += char_width_(text[i]) + char_spacing_(num_chars, text, i); } return cursor; } static i64 enum_text_columns_(i64 num_chars, c32 *text) { if (text == NULL) return 0; i64 cols = 0; i64 n = 0; for (i64 i = 0; i < num_chars; ++i) { if (text[i] <= ' ') { if (text[i] == '\n') { if (cols < n) cols = n; n = 0; } else if (text[i] == '\b' && i > 0) { if (cols < n) cols = n; n -= char_width_(text[i - 1]) + char_spacing_(num_chars, text, i - 1); } else if (text[i] == '\r') { if (cols < n) cols = n; n = 0; } else n += char_width_(' ') + char_spacing_(num_chars, text, i); continue; } n += char_width_(text[i]) + char_spacing_(num_chars, text, i); } if (cols < n) cols = n; return cols; } static i64 enum_text_rows_(i64 num_chars, c32 *text) { if (text == NULL) return 0; i64 rows = 0; for (i64 i = 0; i <= num_chars; ++i) if (i == num_chars || text[i] == '\n') { if (rows > 0) ++rows; rows += CHAR_NUM_BITS_Y_; } return rows; } static void draw_text_(Brush brush, f64 x0, f64 y0, f64 scale_x, f64 scale_y, i64 num_chars, c32 *text) { x0 = transform_x_(brush, x0); y0 = transform_y_(brush, y0); if (brush.scale.x < 0) scale_x *= -brush.scale.x; else scale_x *= brush.scale.x; if (brush.scale.y < 0) scale_y *= -brush.scale.y; else scale_y *= brush.scale.y; f64 kx = scale_x; f64 h = scale_y * CHAR_NUM_BITS_Y_; if (h < EPSILON) return; f64 x = x0; f64 y = y0; for (i64 n = 0; n < num_chars; ++n) { if (text[n] <= ' ') { if (text[n] == '\n') { x = x0; y += scale_y * (CHAR_NUM_BITS_Y_ + 1); } else if (text[n] == '\b' && n > 0) x -= kx * (char_width_(text[n - 1]) + char_spacing_(num_chars, text, n - 1)); else if (text[n] == '\r') x = x0; else x += kx * (char_width_(' ') + char_spacing_(num_chars, text, n)); continue; } i64 num_cols = char_width_(text[n]); f64 w = num_cols * kx; if (w >= EPSILON) { i64 i0 = (i64) floor(x); i64 i1 = (i64) ceil (x + w); i64 j0 = (i64) floor(y); i64 j1 = (i64) ceil (y + h); if (i0 < 0) i0 = 0; if (i1 >= brush.buffer.width) i1 = brush.buffer.width - 1; if (j0 < 0) j0 = 0; if (j1 >= brush.buffer.height) j1 = brush.buffer.height - 1; f64 w_inv = 1. / w; f64 h_inv = 1. / h; for (i64 i = i0; i <= i1; ++i) { i64 column = (i64) floor(((i - x) * num_cols) * w_inv + .5); i64 offset = char_column_offset_(text[n], column); if (brush.xor_color) for (i64 j = j0; j <= j1; ++j) { i64 row = (i64) floor(((j - y) * CHAR_NUM_BITS_Y_) * h_inv + .5); if (char_bit_(offset, row)) put_pixel_xor_(brush, i, j); } else if (brush.alpha) for (i64 j = j0; j <= j1; ++j) { i64 row = (i64) floor(((j - y) * CHAR_NUM_BITS_Y_) * h_inv + .5); if (char_bit_(offset, row)) put_pixel_alpha_(brush, i, j); } else for (i64 j = j0; j <= j1; ++j) { i64 row = (i64) floor(((j - y) * CHAR_NUM_BITS_Y_) * h_inv + .5); if (char_bit_(offset, row)) put_pixel_color_(brush, i, j); } } } x += kx * (num_cols + char_spacing_(num_chars, text, n)); } } static Brush brush_defaults_(Brush b) { if (g_platform.frame_width > 0) { if (b.scale.x == 0.) b.scale.x = ((f64) g_platform.frame_width) / g_platform.real_width; if (b.scale.y == 0.) b.scale.y = ((f64) g_platform.frame_height) / g_platform.real_height; } if (b.buffer.pixels == NULL) b.buffer = frame_pixels(); if (b.buffer.line_size == 0) b.buffer.line_size = b.buffer.width; return b; } // ================================================================ vec3_f32 vec3_from_vec4_f32(vec4_f32 v) { return (vec3_f32) { .x = v.x, .y = v.y, .z = v.z, }; } vec4_f32 vec4_from_vec3_f32(vec3_f32 v, f32 w) { return (vec4_f32) { .x = v.x, .y = v.y, .z = v.z, .w = w, }; } vec3_f32 vec3_f32_lerp(vec3_f32 a, vec3_f32 b, f32 t) { return (vec3_f32) { .x = a.x + (b.x - a.x) * t, .y = a.y + (b.y - a.y) * t, .z = a.z + (b.z - a.z) * t, }; } vec4_f32 vec4_f32_lerp(vec4_f32 a, vec4_f32 b, f32 t) { return (vec4_f32) { .x = a.x + (b.x - a.x) * t, .y = a.y + (b.y - a.y) * t, .z = a.z + (b.z - a.z) * t, .w = a.w + (b.w - a.w) * t, }; } // ================================================================ vec3_f32 rgb_gamma_add(vec3_f32 rgb) { return (vec3_f32) { .x = (f32) gamma_(rgb.x), .y = (f32) gamma_(rgb.y), .z = (f32) gamma_(rgb.z), }; } vec3_f32 rgb_gamma_remove(vec3_f32 rgb) { return (vec3_f32) { .x = (f32) gamma_re_(rgb.x), .y = (f32) gamma_re_(rgb.y), .z = (f32) gamma_re_(rgb.z), }; } vec4_f32 rgba_gamma_add(vec4_f32 rgba) { return (vec4_f32) { .x = (f32) gamma_(rgba.x), .y = (f32) gamma_(rgba.y), .z = (f32) gamma_(rgba.z), .w = (f32) gamma_(rgba.w), }; } vec4_f32 rgba_gamma_remove(vec4_f32 rgba) { return (vec4_f32) { .x = (f32) gamma_re_(rgba.x), .y = (f32) gamma_re_(rgba.y), .z = (f32) gamma_re_(rgba.z), .w = (f32) gamma_re_(rgba.w), }; } vec3_f32 lab_from_rgb(vec3_f32 rgb) { f64 l = 0.4122214708 * rgb.x + 0.5363325363 * rgb.y + 0.0514459929 * rgb.z; f64 m = 0.2119034982 * rgb.x + 0.6806995451 * rgb.y + 0.1073969566 * rgb.z; f64 s = 0.0883024619 * rgb.x + 0.2817188376 * rgb.y + 0.6299787005 * rgb.z; f64 l_ = cbrt(l); f64 m_ = cbrt(m); f64 s_ = cbrt(s); return (vec3_f32) { .x = (f32) (0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_), .y = (f32) (1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_), .z = (f32) (0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_), }; } vec3_f32 rgb_from_lab(vec3_f32 lab) { f64 l_ = lab.x + 0.3963377774 * lab.y + 0.2158037573 * lab.z; f64 m_ = lab.x - 0.1055613458 * lab.y - 0.0638541728 * lab.z; f64 s_ = lab.x - 0.0894841775 * lab.y - 1.2914855480 * lab.z; f64 l = l_ * l_ * l_; f64 m = m_ * m_ * m_; f64 s = s_ * s_ * s_; return (vec3_f32) { .x = (f32) (+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s), .y = (f32) (-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s), .z = (f32) (-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s), }; } vec3_f32 lch_from_lab(vec3_f32 lab) { f64 a = lab.y; f64 b = lab.z; return (vec3_f32) { .x = lab.x, .y = (f32) sqrt (a * a + b * b), .z = (f32) atan2(b, a), }; } vec3_f32 lab_from_lch(vec3_f32 lch) { return (vec3_f32) { .x = lch.x, .y = (f32) (lch.y * cos(lch.z)), .z = (f32) (lch.y * sin(lch.z)), }; } vec3_f32 rgb_from_lch(vec3_f32 lch) { return rgb_from_lab(lab_from_lch(lch)); } vec3_f32 lch_from_rgb(vec3_f32 rgb) { return lch_from_lab(lab_from_rgb(rgb)); } vec3_f32 rgb_gamma_lerp (vec3_f32 a, vec3_f32 b, f32 t) { return rgb_gamma_add(vec3_f32_lerp( rgb_gamma_remove(a), rgb_gamma_remove(b), t )); } vec4_f32 rgba_gamma_lerp(vec4_f32 a, vec4_f32 b, f32 t) { return rgba_gamma_add(vec4_f32_lerp( rgba_gamma_remove(a), rgba_gamma_remove(b), t )); } vec3_f32 rgb_mix(vec3_f32 a, vec3_f32 b, f32 t) { vec3_f32 a_lch = lch_from_rgb(a); vec3_f32 b_lch = lch_from_rgb(b); return rgb_from_lch((vec3_f32) { a_lch.x + (b_lch.x - a_lch.x) * t, a_lch.y + (b_lch.y - a_lch.y) * t, lch_from_rgb(rgb_gamma_lerp(a, b, t)).z, }); } vec4_f32 rgba_mix(vec4_f32 a, vec4_f32 b, f32 t) { vec3_f32 a_lch = lch_from_rgb(vec3_from_vec4_f32(a)); vec3_f32 b_lch = lch_from_rgb(vec3_from_vec4_f32(b)); vec4_f32 ab = rgba_gamma_lerp(a, b, t); return vec4_from_vec3_f32(rgb_from_lch((vec3_f32) { a_lch.x + (b_lch.x - a_lch.x) * t, a_lch.y + (b_lch.y - a_lch.y) * t, lch_from_rgb(vec3_from_vec4_f32(ab)).z, }), ab.w); } vec3_f32 lch_mix(vec3_f32 a, vec3_f32 b, f32 t) { return (vec3_f32) { a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, lch_from_rgb(rgb_gamma_lerp(rgb_from_lch(a), rgb_from_lch(b), t)).z, }; } vec3_f32 lch_lerp(vec3_f32 a, vec3_f32 b, f32 t) { f32 delta_hue = b.z - a.z; if (delta_hue > M_PI) delta_hue -= M_PI * 2; if (delta_hue < -M_PI) delta_hue += M_PI * 2; return (vec3_f32) { .x = a.x + (b.x - a.x) * t, .y = a.y + (b.y - a.y) * t, .z = a.z + delta_hue * t, }; } u32 rgb_u32_from_f32(vec3_f32 color) { i32 ir = (i32) floor(color.x * 255. + .5); i32 ig = (i32) floor(color.y * 255. + .5); i32 ib = (i32) floor(color.z * 255. + .5); u32 r = ir < 0 ? 0u : ir > 255 ? 255u : (u32) ir; u32 g = ig < 0 ? 0u : ig > 255 ? 255u : (u32) ig; u32 b = ib < 0 ? 0u : ib > 255 ? 255u : (u32) ib; return (r << 16) | (g << 8) | b; } vec3_f32 rgb_f32_from_u32(u32 color) { return (vec3_f32) { .x = ((color & 0xff0000) >> 16) / 255.f, .y = ((color & 0x00ff00) >> 8) / 255.f, .z = (color & 0x0000ff) / 255.f, }; } u32 rgba_u32_from_f32(vec4_f32 color) { i32 ir = (i32) floor(color.x * 255. + .5); i32 ig = (i32) floor(color.y * 255. + .5); i32 ib = (i32) floor(color.z * 255. + .5); i32 ia = (i32) floor(color.w * 255. + .5); u32 r = ir < 0 ? 0u : ir > 255 ? 255u : (u32) ir; u32 g = ig < 0 ? 0u : ig > 255 ? 255u : (u32) ig; u32 b = ib < 0 ? 0u : ib > 255 ? 255u : (u32) ib; u32 a = ia < 0 ? 0u : ia > 255 ? 255u : (u32) ia; return (a << 24) | (r << 16) | (g << 8) | b; } vec4_f32 rgba_f32_from_u32(u32 color) { return (vec4_f32) { .x = ((color & 0x00ff0000) >> 16) / 255.f, .y = ((color & 0x0000ff00) >> 8) / 255.f, .z = (color & 0x000000ff) / 255.f, .w = ((color & 0xff000000) >> 24) / 255.f, }; } b8 rectangle_contains(f64 x0, f64 y0, f64 width, f64 height, f64 px, f64 py) { return px >= x0 && px < x0 + width && py >= y0 && py < y0 + height; } b8 triangle_contains(f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2, f64 px, f64 py) { // Z-components of cross-products // f64 x10 = x1 - x0; f64 x21 = x2 - x1; f64 x02 = x0 - x2; f64 y10 = y1 - y0; f64 y21 = y2 - y1; f64 y02 = y0 - y2; f64 z0 = x02 * y10 - x10 * y02; f64 z1 = x10 * y21 - x21 * y10; f64 z2 = x21 * y02 - x02 * y21; f64 pz0 = x02 * (py - y0) - (px - x0) * y02; f64 pz1 = x10 * (py - y1) - (px - x1) * y10; f64 pz2 = x21 * (py - y2) - (px - x2) * y21; return same_sign_(z0, pz0) && same_sign_(z1, pz1) && same_sign_(z2, pz2); } b8 quad_contains(f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2, f64 x3, f64 y3, f64 px, f64 py) { return triangle_contains(x0, y0, x1, y1, x2, y2, px, py) || triangle_contains(x0, y0, x2, y2, x3, y3, px, py); } b8 ellipse_contains(f64 x0, f64 y0, f64 width, f64 height, f64 px, f64 py) { f64 dw = width / 2; f64 dh = height / 2; if (dw < EPSILON || dh < EPSILON) return 0; f64 cx = x0 + dw; f64 cy = y0 + dh; f64 kx = 1. / dw; f64 ky = 1. / dh; f64 dx = (px - cx) * kx; f64 dy = (py - cy) * ky; return dx * dx + dy * dy - 1.0 < EPSILON; } b8 line_contains(f64 x0, f64 y0, f64 x1, f64 y1, f64 width, f64 px, f64 py) { f64 dx = x1 - x0; f64 dy = y1 - y0; // Tangent // f64 tx = -dy; f64 ty = dx; f64 tl = sqrt(tx * tx + ty * ty); if (tl >= EPSILON) { tx /= tl; ty /= tl; } tx *= width * .5; ty *= width * .5; return triangle_contains(x0 - tx, y0 - ty, x0 + tx, y0 + ty, x1 + tx, y1 + ty, px, py) || triangle_contains(x0 - tx, y0 - ty, x1 + tx, y1 + ty, x1 - tx, y1 - ty, px, py); } Pixel_Buffer frame_pixels(void) { return (Pixel_Buffer) { .width = g_platform.frame_width, .height = g_platform.frame_height, .line_size = g_platform.frame_width, .pixels = g_platform.pixels, }; } Pixel_Buffer sketch_pixels(i64 width, i64 height) { if (width <= 0 || height <= 0) { width = g_platform.frame_width; height = g_platform.frame_height; } if (width * height > MAX_NUM_PIXELS) { width = 0; height = 0; } return (Pixel_Buffer) { .width = width, .height = height, .line_size = width, .pixels = g_platform.sketch, }; } Pixel_Buffer subimage(Pixel_Buffer image, i64 x, i64 y, i64 width, i64 height) { if (x < 0 || y < 0 || width <= 0 || height <= 0 || x + width > image.width || y + height > image.height || image.pixels == NULL) return (Pixel_Buffer) { .width = 0, .height = 0, .line_size = 0, // Set pixels pointer to prevent default initialization. .pixels = g_platform.sketch, }; return (Pixel_Buffer) { .width = width, .height = height, .line_size = image.line_size, .pixels = image.pixels + (y * image.line_size + x), }; } void put_pixel(Brush brush, i64 x, i64 y) { brush = brush_defaults_(brush); if (x < 0 || x >= brush.buffer.width || y < 0 || y >= brush.buffer.height) return; put_pixel_(brush, x, y); } void draw_pixels(Brush brush, f64 x, f64 y, f64 width, f64 height, Pixel_Buffer src) { if (width < EPSILON || height < EPSILON || src.width <= 0 || src.height <= 0 || src.line_size <= 0 || src.pixels == NULL) return; draw_pixels_(brush_defaults_(brush), x, y, width, height, src); } void fill_rectangle(Brush brush, f64 x, f64 y, f64 width, f64 height) { fill_rectangle_(brush_defaults_(brush), x, y, width, height); } void fill_triangle(Brush brush, f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2) { brush = brush_defaults_(brush); if (!brush.quick) { Brush aa = antialiasing_brush_(brush); f64 x = min3_(x0, x1, x2); f64 y = min3_(y0, y1, y2); f64 width = max3_(x0, x1, x2) - x; f64 height = max3_(y0, y1, y2) - y; Brush clear = aa; clear.color = vec4_from_vec3_f32(vec3_from_vec4_f32(brush.color), 0.f); fill_rectangle_(clear, 0, 0, width, height); fill_triangle_(aa, x0 - x, y0 - y, x1 - x, y1 - y, x2 - x, y2 - y); f64 x0, y0, x1, y1; transform_box_(aa, 0, 0, width, height, &x0, &y0, &x1, &y1); i64 i0 = (i64) floor(x0); i64 i1 = (i64) ceil (x1); i64 j0 = (i64) floor(y0); i64 j1 = (i64) ceil (y1); draw_pixels_from_( copy_brush_(brush), x, y, width, height, aa.buffer, i0, j0, i1 - i0, j1 - j0 ); } else fill_triangle_(brush, x0, y0, x1, y1, x2, y2); } void fill_quad(Brush brush, f64 x0, f64 y0, f64 x1, f64 y1, f64 x2, f64 y2, f64 x3, f64 y3) { brush = brush_defaults_(brush); if (!brush.quick) { Brush aa = antialiasing_brush_(brush); f64 x = min4_(x0, x1, x2, x3); f64 y = min4_(y0, y1, y2, y3); f64 width = max4_(x0, x1, x2, x3) - x; f64 height = max4_(y0, y1, y2, y3) - y; Brush clear = aa; clear.color = vec4_from_vec3_f32(vec3_from_vec4_f32(brush.color), 0.f); fill_rectangle_(clear, 0, 0, width, height); fill_quad_(aa, x0 - x, y0 - y, x1 - x, y1 - y, x2 - x, y2 - y, x3 - x, y3 - y); f64 x0, y0, x1, y1; transform_box_(aa, 0, 0, width, height, &x0, &y0, &x1, &y1); i64 i0 = (i64) floor(x0); i64 i1 = (i64) ceil (x1); i64 j0 = (i64) floor(y0); i64 j1 = (i64) ceil (y1); draw_pixels_from_( copy_brush_(brush), x, y, width, height, aa.buffer, i0, j0, i1 - i0, j1 - j0 ); } else fill_quad_(brush, x0, y0, x1, y1, x2, y2, x3, y3); } void fill_ellipse(Brush brush, f64 x, f64 y, f64 width, f64 height) { // FIXME PERF: // Implement better algorithm. if (width < EPSILON || height < EPSILON) return; brush = brush_defaults_(brush); if (!brush.quick) { Brush aa = antialiasing_brush_(brush); Brush clear = aa; clear.color = vec4_from_vec3_f32(vec3_from_vec4_f32(brush.color), 0.f); fill_rectangle_(clear, 0, 0, width, height); fill_ellipse_(aa, 0, 0, width, height); f64 x0, y0, x1, y1; transform_box_(aa, 0, 0, width, height, &x0, &y0, &x1, &y1); i64 i0 = (i64) floor(x0); i64 i1 = (i64) ceil (x1); i64 j0 = (i64) floor(y0); i64 j1 = (i64) ceil (y1); draw_pixels_from_( copy_brush_(brush), x, y, width, height, aa.buffer, i0, j0, i1 - i0, j1 - j0 ); } else fill_ellipse_(brush, x, y, width, height); } void fill_line(Brush brush, f64 x0, f64 y0, f64 x1, f64 y1, f64 width) { if (width < EPSILON) return; f64 dx = x1 - x0; f64 dy = y1 - y0; // Tangent // f64 tx = -dy; f64 ty = dx; f64 tl = sqrt(tx * tx + ty * ty); if (tl < EPSILON) return; f64 k = .5 / tl; tx *= width * k; ty *= width * k; fill_quad(brush, x0 - tx, y0 - ty, x0 + tx, y0 + ty, x1 + tx, y1 + ty, x1 - tx, y1 - ty); } void draw_text_area(Brush brush, f64 x, f64 y, f64 width, f64 height, f64 max_scale_x, f64 max_scale_y, i64 num_chars, c32 *text) { if (text == NULL || num_chars <= 0 || max_scale_x < EPSILON || max_scale_y < EPSILON) return; i64 num_columns = enum_text_columns_(num_chars, text); i64 num_rows = enum_text_rows_(num_chars, text); f64 scale_x = width / num_columns; f64 scale_y = height / num_rows; f64 kx = scale_x / max_scale_x; f64 ky = scale_y / max_scale_y; f64 k = kx < ky ? kx : ky; kx = k * max_scale_x; ky = k * max_scale_y; brush = brush_defaults_(brush); if (!brush.quick) { Brush aa = antialiasing_brush_(brush); Brush clear = aa; clear.color = vec4_from_vec3_f32(vec3_from_vec4_f32(brush.color), 0.f); fill_rectangle_(clear, 0, 0, width, height); draw_text_(aa, 0, 0, kx, ky, num_chars, text); f64 x0, y0, x1, y1; transform_box_(aa, 0, 0, width, height, &x0, &y0, &x1, &y1); i64 i0 = (i64) floor(x0); i64 i1 = (i64) ceil (x1); i64 j0 = (i64) floor(y0); i64 j1 = (i64) ceil (y1); draw_pixels_from_( copy_brush_(brush), x, y, width, height, aa.buffer, i0, j0, i1 - i0, j1 - j0 ); } else draw_text_(brush, x, y, kx, ky, num_chars, text); } void draw_text_cursor(Brush brush, f64 x, f64 y, f64 width, f64 height, f64 max_scale_x, f64 max_scale_y, i64 cursor, i64 selection, i64 num_chars, c32 *text) { if (max_scale_x < EPSILON || max_scale_y < EPSILON) return; i64 num_columns = enum_text_columns_(num_chars, text); i64 num_rows = enum_text_rows_(num_chars, text); i64 cursor_x = text_cursor_(cursor, text); i64 cursor_y = enum_text_rows_(cursor, text); f64 scale_x = width / num_columns; f64 scale_y = height / num_rows; f64 kx = scale_x / max_scale_x; f64 ky = scale_y / max_scale_y; f64 k = kx < ky ? kx : ky; kx = k * max_scale_x; ky = k * max_scale_y; brush = brush_defaults_(brush); if (selection != 0) { i64 selection_x, selection_y; if (selection > 0) { selection_x = text_cursor_(cursor + selection, text); selection_y = enum_text_rows_(cursor + selection, text); } else { selection_x = cursor_x; selection_y = cursor_y; cursor_x = text_cursor_(cursor + selection, text); cursor_y = enum_text_rows_(cursor + selection, text); } if (cursor_y == selection_y) fill_rectangle_( brush, x + kx * cursor_x, y + ky * cursor_y - ky * (CHAR_NUM_BITS_Y_ + 1), kx * (selection_x - cursor_x), ky * (CHAR_NUM_BITS_Y_ + 1) ); else { fill_rectangle_( brush, x + kx * cursor_x, y + ky * cursor_y - ky * (CHAR_NUM_BITS_Y_ + 1), kx * (num_columns - cursor_x), ky * (CHAR_NUM_BITS_Y_ + 1) ); for (i64 j = cursor_y + CHAR_NUM_BITS_Y_ + 1; j < selection_y; j += CHAR_NUM_BITS_Y_ + 1) fill_rectangle_( brush, x, y + ky * j - ky * (CHAR_NUM_BITS_Y_ + 1), kx * num_columns, ky * (CHAR_NUM_BITS_Y_ + 1) ); fill_rectangle_( brush, x, y + ky * selection_y - ky * (CHAR_NUM_BITS_Y_ + 1), kx * selection_x, ky * (CHAR_NUM_BITS_Y_ + 1) ); } } else fill_rectangle_( brush, x + kx * cursor_x, y + ky * cursor_y - ky * CHAR_NUM_BITS_Y_, kx * .5, ky * (CHAR_NUM_BITS_Y_ - 1) ); } // ================================================================ // // Test suite // // ================================================================ #ifdef GRAPHICS_TEST_SUITE #define TEST_FILE graphics #include "test.c" TEST("colors") { REQUIRE_EQ(rgb_u32_from_f32((vec3_f32) { 1.f, 0.f, 0.f, }), 0xff0000); REQUIRE_EQ(rgb_u32_from_f32((vec3_f32) { 0.f, 1.f, 0.f, }), 0x00ff00); REQUIRE_EQ(rgb_u32_from_f32((vec3_f32) { 0.f, 0.f, 1.f, }), 0x0000ff); REQUIRE_EQ(rgb_u32_from_f32((vec3_f32) { .5f, 0.f, 0.f, }), 0x800000); REQUIRE_EQ(rgb_u32_from_f32((vec3_f32) { 0.f, .5f, 0.f, }), 0x008000); REQUIRE_EQ(rgb_u32_from_f32((vec3_f32) { 0.f, 0.f, .5f, }), 0x000080); REQUIRE_EQ(rgba_u32_from_f32((vec4_f32) { 1.f, 0.f, 0.f, 0.f, }), 0x00ff0000); REQUIRE_EQ(rgba_u32_from_f32((vec4_f32) { 0.f, 1.f, 0.f, 0.f, }), 0x0000ff00); REQUIRE_EQ(rgba_u32_from_f32((vec4_f32) { 0.f, 0.f, 1.f, 0.f, }), 0x000000ff); REQUIRE_EQ(rgba_u32_from_f32((vec4_f32) { 0.f, 0.f, 0.f, 1.f, }), 0xff000000); REQUIRE_EQ(rgba_u32_from_f32((vec4_f32) { .5f, 0.f, 0.f, 0.f, }), 0x00800000); REQUIRE_EQ(rgba_u32_from_f32((vec4_f32) { 0.f, .5f, 0.f, 0.f, }), 0x00008000); REQUIRE_EQ(rgba_u32_from_f32((vec4_f32) { 0.f, 0.f, .5f, 0.f, }), 0x00000080); REQUIRE_EQ(rgba_u32_from_f32((vec4_f32) { 0.f, 0.f, 0.f, .5f, }), 0x80000000); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 0.f, 0.f, 0.f, }).x * 100, 0); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 1.f, 1.f, 1.f, }).x * 100, 100); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 1.f, 0.f, 0.f, }).x * 100, 62); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 0.f, 1.f, 0.f, }).x * 100, 86); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 0.f, 0.f, 1.f, }).x * 100, 45); REQUIRE_EQ(lch_from_rgb((vec3_f32) { .5f, .5f, .5f, }).y * 100, 0); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 1.f, 0.f, 0.f, }).y * 100, 25); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 0.f, 1.f, 0.f, }).y * 100, 29); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 0.f, 0.f, 1.f, }).y * 100, 31); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 1.f, 0.f, 0.f, }).z * 100, 51); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 0.f, 1.f, 0.f, }).z * 100, 248); REQUIRE_EQ(lch_from_rgb((vec3_f32) { 0.f, 0.f, 1.f, }).z * 100, -167); vec3_f32 white = rgb_from_lch(lch_from_rgb((vec3_f32) { 1.f, 1.f, 1.f, })); REQUIRE_EQ(white.x * 100, 100); REQUIRE_EQ(white.y * 100, 100); REQUIRE_EQ((white.z + 3e-7) * 100, 100); vec3_f32 black = rgb_from_lch(lch_from_rgb((vec3_f32) { 0.f, 0.f, 0.f, })); REQUIRE_EQ(black.x * 100, 0); REQUIRE_EQ(black.y * 100, 0); REQUIRE_EQ(black.z * 100, 0); vec3_f32 grey = rgb_from_lch(lch_from_rgb((vec3_f32) { .5f, .5f, .5f, })); REQUIRE_EQ(grey.x * 100, 50); REQUIRE_EQ((grey.y + 3e-8) * 100, 50); REQUIRE_EQ((grey.z + 3e-7) * 100, 50); vec3_f32 red = rgb_from_lch(lch_from_rgb((vec3_f32) { 1.f, 0.f, 0.f, })); REQUIRE_EQ(red.x * 100, 100); REQUIRE_EQ(red.y * 100, 0); REQUIRE_EQ(red.z * 100, 0); vec3_f32 green = rgb_from_lch(lch_from_rgb((vec3_f32) { 0.f, 1.f, 0.f, })); REQUIRE_EQ(green.x * 100, 0); REQUIRE_EQ(green.y * 100, 100); REQUIRE_EQ(green.z * 100, 0); vec3_f32 blue = rgb_from_lch(lch_from_rgb((vec3_f32) { 0.f, 0.f, 1.f, })); REQUIRE_EQ(blue.x * 100, 0); REQUIRE_EQ(blue.y * 100, 0); REQUIRE_EQ((blue.z + 6e-8) * 100, 100); vec3_f32 yellow = rgb_from_lch(lch_from_rgb((vec3_f32) { 1.f, 1.f, 0.f, })); REQUIRE_EQ(yellow.x * 100, 100); REQUIRE_EQ((yellow.y + 6e-8) * 100, 100); REQUIRE_EQ(yellow.z * 100, 0); vec3_f32 cyan = rgb_from_lch(lch_from_rgb((vec3_f32) { 0.f, 1.f, 1.f, })); REQUIRE_EQ(cyan.x * 100, 0); REQUIRE_EQ((cyan.y + 2e-7) * 100, 100); REQUIRE_EQ((cyan.z + 2e-7) * 100, 100); vec3_f32 purple = rgb_from_lch(lch_from_rgb((vec3_f32) { 1.f, 0.f, 1.f, })); REQUIRE_EQ(purple.x * 100, 100); REQUIRE_EQ(purple.y * 100, 0); REQUIRE_EQ((purple.z + 2e-7) * 100, 100); } Brush brush = { .buffer = { .width = 1280, .height = 720, .line_size = 1280, .pixels = g_platform.pixels, }, .position = { 0.f, 0.f, }, .scale = { 1.f, 1.f, }, .color = { 1.f, 1.f, 1.f, 1.f }, }; BENCHMARK("fill rectangle") { BENCHMARK_BEGIN; { fill_rectangle(brush, 100, 100, 300, 200); } BENCHMARK_END; } BENCHMARK("fill triangle") { BENCHMARK_BEGIN; { fill_triangle(brush, 100, 100, 300, 100, 200, 250); } BENCHMARK_END; } BENCHMARK("fill quad") { BENCHMARK_BEGIN; { fill_quad(brush, 100, 100, 300, 100, 300, 200, 100, 200); } BENCHMARK_END; } BENCHMARK("fill ellipse") { BENCHMARK_BEGIN; { fill_ellipse(brush, 80, 80, 340, 240); } BENCHMARK_END; } BENCHMARK("fill line") { BENCHMARK_BEGIN; { fill_line(brush, 100, 100, 300, 200, 40); } BENCHMARK_END; } BENCHMARK("draw text area") { BENCHMARK_BEGIN; { c32 text[] = { 'H', 'e', 'l', 'l', 'o', ',', ' ', 'S', 'a', 'i', 'l', 'o', 'r', '!', }; draw_text_area(brush, 100, 100, 300, 200, 100, 200, sizeof text / sizeof *text, text); } BENCHMARK_END; } void update_and_render_frame(void) {} i32 main(i32 argc, c8 **argv) { return run_tests_and_benchmarks(argc, argv); } #undef TEST_FILE #endif // GRAPHICS_TEST_SUITE // ================================================================ #endif // GRAPHICS_IMPL_GUARD_ #endif // GRAPHICS_HEADER