"""Примитивы пиксель-арта: границы, детерминизм, порядок по глубине.""" import numpy as np import pytest from spriteforge import pixel def test_colors_are_parsed_and_validated(): assert list(pixel.to_rgba("#102030")) == [16, 32, 48, 255] assert list(pixel.to_rgba("#10203040")) == [16, 32, 48, 64] assert list(pixel.to_rgba((1, 2, 3))) == [1, 2, 3, 255] for bad in ("102030", "#1020", "#GGGGGG"): with pytest.raises(ValueError): pixel.to_rgba(bad) def test_canvas_starts_transparent_and_fills(): image = pixel.canvas(5, 3) assert image.shape == (3, 5, 4) and image.dtype == np.uint8 and not image.any() pixel.fill(image, "#204060") assert (image[..., 3] == 255).all() with pytest.raises(ValueError): pixel.canvas(0, 4) def test_fill_blends_over_the_whole_canvas(): image = pixel.canvas(4, 4, "#000000") pixel.fill(image, "#FFFFFF80", blend=True) assert image[0, 0, 0] == 128 and image[0, 0, 3] == 255 def test_set_pixel_ignores_everything_outside_the_canvas(): image = pixel.canvas(4, 4) for x, y in ((-1, 0), (0, -1), (4, 0), (0, 4)): pixel.set_pixel(image, x, y, "#FFFFFF") assert not image.any() pixel.set_pixel(image, 3, 3, "#FFFFFF") assert list(image[3, 3]) == [255, 255, 255, 255] def test_lines_stay_inside_the_canvas_and_thicken(): image = pixel.canvas(9, 9) pixel.draw_line(image, -5, 4, 20, 4, "#FFFFFF") assert (image[4, :, 3] == 255).all() and image[3, :, 3].sum() == 0 thick = pixel.canvas(9, 9) pixel.draw_line(thick, 4, 1, 4, 7, "#FFFFFF", thickness=3) assert np.count_nonzero(thick[..., 3]) > np.count_nonzero(image[..., 3]) assert pixel.line_points(0, 0, 2, 2) == ((0, 0), (1, 1), (2, 2)) def test_rectangles_and_ellipses_support_outlines(): image = pixel.canvas(10, 10) pixel.draw_rect(image, 2, 2, 7, 7, "#FFFFFF", filled=False) assert image[2, 2, 3] == 255 and image[4, 4, 3] == 0 pixel.draw_rect(image, -3, -3, 1, 1, "#FF0000") assert image[0, 0, 3] == 255 ellipse = pixel.canvas(11, 11) pixel.draw_ellipse(ellipse, 5, 5, 4, 3, "#FFFFFF") assert ellipse[5, 1, 3] == 255 and ellipse[1, 5, 3] == 0 hollow = pixel.canvas(11, 11) pixel.draw_ellipse(hollow, 5, 5, 4, 3, "#FFFFFF", filled=False, thickness=1) assert hollow[5, 5, 3] == 0 and hollow[5, 1, 3] == 255 def test_capsule_covers_only_its_own_radius(): image = pixel.canvas(12, 12) pixel.draw_capsule(image, 3, 6, 8, 6, 3.0, "#FFFFFF") assert image[6, 3, 3] == 255 and image[6, 8, 3] == 255 assert image[6, 11, 3] == 0 and image[0, 3, 3] == 0 mask = pixel.capsule_mask(12, 12, 3, 6, 3, 6, 5.0) assert mask[6, 3] and not mask[6, 9] def test_outline_marks_only_the_silhouette_edge(): image = pixel.canvas(9, 9) pixel.draw_rect(image, 2, 2, 6, 6, "#FFFFFF") pixel.outline(image, "#000000") assert list(image[2, 2, :3]) == [0, 0, 0] assert list(image[4, 4, :3]) == [255, 255, 255] # Силуэт, упирающийся в край холста, всё равно получает кромку с этой стороны. touching = pixel.edge_mask(pixel.canvas(4, 4, "#FFFFFF")) assert touching[0].all() and touching[:, 0].all() and touching[-1].all() assert not touching[1:3, 1:3].any() def test_shading_ramp_and_depth_factor(): shadow, base, highlight = pixel.ramp("#808080") assert int(shadow[0]) < int(base[0]) < int(highlight[0]) assert int(pixel.shade("#FFFFFF", 2.0)[0]) == 255 and int(pixel.shade("#804020", 1.0)[3]) == 255 assert pixel.depth_factor(0.0) < pixel.depth_factor(0.5) < pixel.depth_factor(1.0) == 1.0 def test_far_limbs_are_drawn_first_and_darker(): near = pixel.Limb(2, 2, 2, 9, 3.0, "#A0A0A0", depth=1.0) far = pixel.Limb(6, 2, 6, 9, 3.0, "#A0A0A0", depth=0.0) assert pixel.sort_by_depth((near, far)) == (far, near) image = pixel.canvas(12, 12) pixel.draw_limbs(image, (near, far)) assert int(image[5, 6, 0]) < int(image[5, 2, 0]) def test_overlapping_limbs_let_the_near_one_win(): image = pixel.canvas(12, 12) far = pixel.Limb(2, 6, 9, 6, 3.0, "#FFFFFF", depth=0.0) near = pixel.Limb(2, 6, 9, 6, 3.0, "#FFFFFF", depth=1.0) pixel.draw_limbs(image, (far, near)) assert int(image[6, 5, 0]) == 255 def test_rim_light_touches_only_the_lit_side(): image = pixel.canvas(16, 16) pixel.draw_ellipse(image, 8, 8, 5, 5, "#404040") lit = image.copy() pixel.rim_light(lit, (-1.0, 0.0), "#FFFFFF", strength=1.0) left = int(lit[8, 3, 0]) - int(image[8, 3, 0]) right = int(lit[8, 13, 0]) - int(image[8, 13, 0]) assert left > 0 and right == 0 assert (lit[..., 3] == image[..., 3]).all() with pytest.raises(ValueError): pixel.rim_light(lit, (0.0, 0.0)) def test_noise_uses_only_the_supplied_generator(): def painted(): image = pixel.canvas(8, 8) pixel.draw_rect(image, 1, 1, 6, 6, "#606060") return image first, second, other = painted(), painted(), painted() pixel.add_noise(first, np.random.default_rng(4)) pixel.add_noise(second, np.random.default_rng(4)) pixel.add_noise(other, np.random.default_rng(5)) assert np.array_equal(first, second) and not np.array_equal(first, other) assert not first[0, 0].any() def test_flip_keeps_the_pivot_on_the_same_pixel(): image = pixel.canvas(6, 3) pixel.set_pixel(image, 1, 1, "#FFFFFF") flipped = pixel.flip_horizontal(image) assert flipped[1, 4, 3] == 255 assert pixel.flip_pivot_x(1, 6) == 4 assert flipped.flags["C_CONTIGUOUS"]