/* Lienzo, herramientas de dibujo, relleno y transformaciones. */ #include #include "tgb_test.h" TEST(canvas_basics) { CHECK(tgb_canvas_create(0, 10) == NULL); CHECK(tgb_canvas_create(10, -1) == NULL); CHECK(tgb_canvas_create(TGB_MAX_DIMENSION + 1, 10) == NULL); tgb_canvas *c = tgb_canvas_create(8, 6); CHECK(c != NULL); CHECK_EQ(tgb_canvas_width(c), 8); CHECK_EQ(tgb_canvas_bytes(c), 8 * 6 * 4); CHECK(tgb_canvas_is_empty(c)); tgb_canvas_set(c, 2, 3, RED); CHECK(is_px(c, 2, 3, RED)); CHECK(!tgb_canvas_is_empty(c)); CHECK_EQ(tgb_canvas_get(c, -1, 0).a, 0); /* fuera del lienzo = transparente */ CHECK_EQ(tgb_canvas_get(c, 8, 0).a, 0); tgb_canvas_set(c, 100, 100, RED); /* fuera: se ignora sin fallar */ tgb_canvas *d = tgb_canvas_clone(c); CHECK(tgb_canvas_equal(c, d)); tgb_canvas_set(d, 0, 0, BLUE); CHECK(!tgb_canvas_equal(c, d)); CHECK_EQ(tgb_canvas_refs(c), 1); tgb_canvas_retain(c); CHECK_EQ(tgb_canvas_refs(c), 2); tgb_canvas_release(c); tgb_canvas_release(c); tgb_canvas_release(d); tgb_canvas_release(NULL); /* aceptar NULL */ } TEST(canvas_blend_math) { tgb_canvas *c = tgb_canvas_create(2, 1); tgb_color half_red = {255, 0, 0, 128}; tgb_canvas_blend(c, 0, 0, half_red); /* sobre transparente: queda igual */ CHECK(is_px(c, 0, 0, half_red)); tgb_canvas_set(c, 1, 0, BLUE); tgb_canvas_blend(c, 1, 0, half_red); /* 50 % rojo sobre azul opaco */ tgb_color p = tgb_canvas_get(c, 1, 0); CHECK_EQ(p.a, 255); CHECK(abs(p.r - 128) <= 1 && abs(p.b - 127) <= 1 && p.g == 0); tgb_canvas_erase_px(c, 1, 0, 255); /* borrado total: forma canónica 0,0,0,0 */ CHECK(is_px(c, 1, 0, (tgb_color){0, 0, 0, 0})); tgb_canvas_release(c); } TEST(canvas_crop_blit_dirty) { tgb_canvas *c = tgb_canvas_create(10, 10); CHECK_EQ(tgb_canvas_dirty(c).w, 0); tgb_canvas_set(c, 3, 4, RED); tgb_canvas_set(c, 6, 8, RED); tgb_rect d = tgb_canvas_dirty(c); CHECK(d.x == 3 && d.y == 4 && d.w == 4 && d.h == 5); tgb_canvas_reset_dirty(c); CHECK_EQ(tgb_canvas_dirty(c).w, 0); tgb_canvas *crop = NULL; CHECK_OK(tgb_canvas_crop(c, (tgb_rect){3, 4, 4, 5}, &crop)); CHECK(is_px(crop, 0, 0, RED) && is_px(crop, 3, 4, RED)); tgb_canvas *bad = NULL; CHECK_STATUS(tgb_canvas_crop(c, (tgb_rect){8, 8, 5, 5}, &bad), TGB_ERR_RANGE); tgb_canvas *dst = tgb_canvas_create(10, 10); tgb_canvas_blit(dst, crop, 1, 1, 128, true); /* mitad de opacidad */ CHECK_EQ(tgb_canvas_get(dst, 1, 1).a, 128); tgb_canvas_blit(dst, crop, 20, 20, 255, true); /* totalmente fuera: no falla */ tgb_canvas_release(crop); tgb_canvas_release(dst); tgb_canvas_release(c); } TEST(pencil_and_brush) { tgb_canvas *c = tgb_canvas_create(20, 20); tgb_brush pen = tgb_brush_pencil(RED, 1); tgb_draw_line(c, &pen, 0, 0, 9, 9); CHECK_EQ(count_alpha(c), 10); /* diagonal exacta de 10 píxeles */ for (int i = 0; i < 10; i++) CHECK(is_px(c, i, i, RED)); tgb_canvas_clear(c); tgb_draw_line(c, &pen, 2, 5, 12, 5); /* horizontal */ CHECK_EQ(count_alpha(c), 11); tgb_canvas_clear(c); tgb_brush fat = tgb_brush_pencil(BLUE, 3); /* lápiz grueso sin suavizado: todo opaco */ tgb_draw_dab(c, &fat, 10.5f, 10.5f); bool all_opaque = true; for (int y = 0; y < 20; y++) for (int x = 0; x < 20; x++) { tgb_color p = tgb_canvas_get(c, x, y); if (p.a && p.a != 255) all_opaque = false; } CHECK(all_opaque); CHECK(count_alpha(c) >= 5 && count_alpha(c) <= 9); tgb_canvas_clear(c); tgb_brush soft = tgb_brush_soft(BLACK, 10.0f, 0.5f); tgb_draw_dab(c, &soft, 10.0f, 10.0f); CHECK_EQ(tgb_canvas_get(c, 10, 10).a, 255); /* centro opaco */ int a = tgb_canvas_get(c, 14, 10).a; CHECK(a > 0 && a < 255); /* borde suave */ CHECK_EQ(tgb_canvas_get(c, 17, 10).a, 0); tgb_brush er = tgb_brush_eraser(6.0f, 1.0f); tgb_draw_dab(c, &er, 10.0f, 10.0f); CHECK_EQ(tgb_canvas_get(c, 10, 10).a, 0); /* el borrador quita opacidad */ tgb_canvas_release(c); } TEST(pencil_robust_inputs) { tgb_canvas *c = tgb_canvas_create(16, 16); tgb_brush pen = tgb_brush_pencil(RED, 1); tgb_draw_line(c, &pen, NAN, 0, 5, 5); /* no debe colgarse ni escribir fuera */ tgb_draw_line(c, &pen, -1e30f, -1e30f, 1e30f, 1e30f); tgb_draw_dab(c, &pen, INFINITY, 3); tgb_draw_line(c, NULL, 0, 0, 1, 1); tgb_draw_dab(NULL, &pen, 0, 0); CHECK(c != NULL); tgb_canvas_release(c); } TEST(shapes) { tgb_canvas *c = tgb_canvas_create(30, 30); tgb_brush pen = tgb_brush_pencil(RED, 1); tgb_draw_rect(c, &pen, (tgb_rect){2, 2, 4, 3}, true); CHECK_EQ(count_alpha(c), 12); tgb_canvas_clear(c); tgb_draw_rect(c, &pen, (tgb_rect){0, 0, 5, 5}, false); CHECK_EQ(count_alpha(c), 16); /* perímetro de 5x5 */ CHECK(!is_px(c, 2, 2, RED)); tgb_canvas_clear(c); tgb_draw_ellipse(c, &pen, (tgb_rect){5, 5, 9, 9}, true); CHECK(is_px(c, 9, 9, RED)); CHECK(!is_px(c, 5, 5, RED)); bool sym = true; /* simetría horizontal */ for (int y = 5; y < 14; y++) for (int x = 5; x < 14; x++) if (is_px(c, x, y, RED) != is_px(c, 18 - x, y, RED)) sym = false; CHECK(sym); tgb_canvas_clear(c); tgb_draw_ellipse(c, &pen, (tgb_rect){5, 5, 15, 9}, false); CHECK(count_alpha(c) > 20); CHECK(!is_px(c, 12, 9, RED)); /* hueco en el centro */ tgb_canvas_clear(c); tgb_draw_bezier(c, &pen, 2, 2, 10, 25, 20, 25, 27, 2); CHECK(is_px(c, 2, 2, RED) && is_px(c, 27, 2, RED)); CHECK(count_alpha(c) > 25); tgb_canvas_release(c); } TEST(flood_fill_and_eyedropper) { tgb_canvas *c = tgb_canvas_create(10, 10); for (int y = 0; y < 10; y++) tgb_canvas_set(c, 5, y, BLACK); /* muro vertical */ size_t n = tgb_flood_fill(c, 1, 1, RED, 0); CHECK_EQ(n, 50); CHECK(is_px(c, 0, 9, RED) && is_px(c, 4, 0, RED)); CHECK(is_px(c, 5, 5, BLACK)); CHECK_EQ(tgb_canvas_get(c, 7, 7).a, 0); /* el otro lado no se toca */ CHECK_EQ(tgb_flood_fill(c, 1, 1, RED, 0), 0); /* mismo color: no hace nada (y no se cuelga) */ CHECK_EQ(tgb_flood_fill(c, -1, 0, RED, 0), 0); tgb_color e = tgb_eyedropper(c, 5, 3); CHECK(e.r == 0 && e.a == 255); /* tolerancia: gris casi igual se rellena con tolerancia alta */ tgb_canvas *g = tgb_canvas_create(4, 1); for (int x = 0; x < 4; x++) { tgb_color p = {(uint8_t)(100 + x * 3), 100, 100, 255}; tgb_canvas_set(g, x, 0, p); } CHECK_EQ(tgb_flood_fill(g, 0, 0, GREEN, 0), 1); tgb_canvas_set(g, 0, 0, (tgb_color){100, 100, 100, 255}); CHECK_EQ(tgb_flood_fill(g, 0, 0, GREEN, 10), 4); tgb_canvas_release(g); tgb_canvas_release(c); } TEST(regions) { tgb_canvas *c = tgb_canvas_create(10, 10); tgb_canvas_fill_rect(c, (tgb_rect){1, 1, 2, 2}, RED, false); CHECK_OK(tgb_region_move(c, (tgb_rect){0, 0, 4, 4}, 3, 2)); CHECK(is_px(c, 4, 3, RED) && is_px(c, 5, 4, RED)); CHECK_EQ(tgb_canvas_get(c, 1, 1).a, 0); CHECK_EQ(count_alpha(c), 4); CHECK_STATUS(tgb_region_move(c, (tgb_rect){50, 50, 2, 2}, 1, 1), TGB_ERR_RANGE); tgb_canvas_clear(c); tgb_canvas_set(c, 1, 1, RED); CHECK_OK(tgb_region_flip(c, (tgb_rect){0, 0, 4, 4}, true)); CHECK(is_px(c, 2, 1, RED) && count_alpha(c) == 1); CHECK_OK(tgb_region_flip(c, (tgb_rect){0, 0, 4, 4}, false)); CHECK(is_px(c, 2, 2, RED) && count_alpha(c) == 1); tgb_canvas_clear(c); tgb_canvas_fill_rect(c, (tgb_rect){0, 0, 2, 2}, BLUE, false); CHECK_OK(tgb_region_scale(c, (tgb_rect){0, 0, 2, 2}, 4, 4)); CHECK_EQ(count_alpha(c), 16); CHECK_STATUS(tgb_region_scale(c, (tgb_rect){0, 0, 2, 2}, 0, 4), TGB_ERR_RANGE); CHECK_OK(tgb_region_clear(c, (tgb_rect){0, 0, 10, 10})); CHECK(tgb_canvas_is_empty(c)); tgb_canvas_release(c); } int main(void) { RUN(canvas_basics); RUN(canvas_blend_math); RUN(canvas_crop_blit_dirty); RUN(pencil_and_brush); RUN(pencil_robust_inputs); RUN(shapes); RUN(flood_fill_and_eyedropper); RUN(regions); TEST_SUMMARY(); }