renderer.py

8.4 kB · python · 209 lines

1import numpy as np2from PIL import Image, ImageDraw3from typing import List, Optional, Tuple4from mrlypy.core.colors import Color5from mrlypy.core.errors import MrlyError6from .geometry import is_hex, get_orientation, Orientation, tile78# TRIANGLES910def triangle_north(x: int, y: int) -> List[Tuple[int, int]]:11    return [(x, 2 * y + 2), (x + 1, 2 * y), (x + 2, 2 * y + 2)]1213def triangle_south(x: int, y: int) -> List[Tuple[int, int]]:14    return [(x, 2 * y), (x + 1, 2 * y + 2), (x + 2, 2 * y)]1516def triangle_east(x: int, y: int) -> List[Tuple[int, int]]:17    return [(2 * x, y), (2 * x, y + 2), (2 * x + 2, y + 1)]1819def triangle_west(x: int, y: int) -> List[Tuple[int, int]]:20    return [(2 * x + 2, y), (2 * x + 2, y + 2), (2 * x, y + 1)]2122# GET TRIANGLES2324def get_triangles(cell, start: int = 0) -> List[Tuple[List[Tuple[int, int]], Tuple[int, int, int, int]]]:25    inner = cell._cell if hasattr(cell, '_cell') else cell26    height, width_grid = inner.types.shape27    colors_grid = inner.colors28    orientation = get_orientation(width_grid, height)29    triangles = []30    for y in range(height):31        for x in range(width_grid):32            r, g, b, a = colors_grid[y, x]33            if a == 0:34                continue35            flip = (x + y + start) % 236            match orientation:37                case Orientation.HORIZONTAL:38                    points = triangle_north(x, y) if flip == 0 else triangle_south(x, y)39                case Orientation.VERTICAL:40                    points = triangle_east(x, y) if flip == 0 else triangle_west(x, y)41            triangles.append((points, (r, g, b, a)))42    return triangles4344# DRAW4546def draw(cell, scale: int = 1, orientation: str = "horizontal", start: int = 0, outline: Optional[Color] = None, width: int = 1) -> Image.Image:47    inner = cell._cell if hasattr(cell, '_cell') else cell48    height, width_grid = inner.types.shape49    colors_grid = inner.colors50    orientation = get_orientation(width_grid, height)51    triangles = []52    for y in range(height):53        for x in range(width_grid):54            r, g, b, a = colors_grid[y, x]55            if a == 0:56                continue57            flip = (x + y + start) % 258            match orientation:59                case Orientation.HORIZONTAL:60                    points = triangle_north(x, y) if flip == 0 else triangle_south(x, y)61                case Orientation.VERTICAL:62                    points = triangle_east(x, y) if flip == 0 else triangle_west(x, y)63            triangles.append((points, (r, g, b, a)))64    if not triangles:65        return Image.new("RGBA", (0, 0))66    all_x = [p[0] for points, _ in triangles for p in points]67    all_y = [p[1] for points, _ in triangles for p in points]68    min_x, max_x = min(all_x), max(all_x)69    min_y, max_y = min(all_y), max(all_y)70    padding = width if outline else 071    img_width = (max_x - min_x + padding * 2) * scale72    img_height = (max_y - min_y + padding * 2) * scale73    image = Image.new("RGBA", (int(img_width), int(img_height)), (0, 0, 0, 0))74    draw_ctx = ImageDraw.Draw(image)75    offset_x = -min_x + padding76    offset_y = -min_y + padding77    outline_rgba = outline.to_rgba() if outline else None78    for points, color in triangles:79        pts = [(int((p[0] + offset_x) * scale), int((p[1] + offset_y) * scale)) for p in points]80        draw_ctx.polygon(pts, fill=color, outline=outline_rgba, width=width)81    return image8283# SVG8485def svg(cell, scale: int = 1, orientation: str = "horizontal", start: int = 0, outline: Optional[Color] = None, width: int = 1) -> str:86    inner = cell._cell if hasattr(cell, '_cell') else cell87    height, width_grid = inner.types.shape88    orientation = get_orientation(width_grid, height)89    colors_grid = inner.colors90    triangles = []91    for y in range(height):92        for x in range(width_grid):93            r, g, b, a = colors_grid[y, x]94            if a == 0:95                continue96            flip = (x + y + start) % 297            match orientation:98                case Orientation.HORIZONTAL:99                    points = triangle_north(x, y) if flip == 0 else triangle_south(x, y)100                case Orientation.VERTICAL:101                    points = triangle_east(x, y) if flip == 0 else triangle_west(x, y)102            triangles.append((points, (r, g, b, a)))103    if not triangles:104        return "<svg></svg>"105    all_x = [p[0] for points, _ in triangles for p in points]106    all_y = [p[1] for points, _ in triangles for p in points]107    min_x, max_x = min(all_x), max(all_x)108    min_y, max_y = min(all_y), max(all_y)109    padding = width if outline else 0110    img_width = (max_x - min_x + padding * 2) * scale111    img_height = (max_y - min_y + padding * 2) * scale112    offset_x = -min_x + padding113    offset_y = -min_y + padding114    elements = [f'<svg width="{int(img_width)}" height="{int(img_height)}" xmlns="http://www.w3.org/2000/svg">']115    for points, (r, g, b, a) in triangles:116        color = Color.rgba_to_hex(r, g, b, a)117        pts_str = " ".join([f"{int((p[0] + offset_x) * scale)},{int((p[1] + offset_y) * scale)}" for p in points])118        if outline:119            outline_hex = outline.to_hex()120            stroke_attr = f' stroke="{outline_hex}" stroke-width="{width}"'121        else:122            stroke_attr = ''123        elements.append(f'<polygon points="{pts_str}" fill="{color}"{stroke_attr}/>')124    elements.append('</svg>')125    return "\n".join(elements)126127# RECT128129def rect_draw(cell, scale: int = 1, start: int = 0) -> Image.Image:130    inner = cell._cell if hasattr(cell, '_cell') else cell131    if not is_hex(inner):132        raise MrlyError("Cell must be a hexagon.")133    tiled_cell = tile(inner, 3, 3)134    tile_h, tile_w = inner.types.shape135    orientation = get_orientation(tile_w, tile_h)136    match orientation:137        case Orientation.HORIZONTAL:138            dx = (3 * (tile_w + 1)) // 4139            dy = tile_h140            row_shift = tile_h // 2141            geom_crop_w = 2 * dx142            geom_crop_h = 2 * dy143            start_geom_x = (tile_w + 1) // 2144            start_geom_y = tile_h145        case Orientation.VERTICAL:146            dx = tile_w147            dy = (3 * (tile_h + 1)) // 4148            row_shift = tile_w // 2149            geom_crop_w = 2 * dx150            geom_crop_h = 2 * dy151            start_geom_x = tile_w152            start_geom_y = (tile_h + 1) // 2153    triangles = get_triangles(tiled_cell, start)154    img_width = geom_crop_w * scale155    img_height = geom_crop_h * scale156    image = Image.new("RGBA", (int(img_width), int(img_height)), (0, 0, 0, 0))157    draw_ctx = ImageDraw.Draw(image)158    offset_x = -start_geom_x159    offset_y = -start_geom_y160    for points, color in triangles:161        pts = []162        for p in points:163            px = (p[0] + offset_x) * scale164            py = (p[1] + offset_y) * scale165            pts.append((px, py))166        draw_ctx.polygon(pts, fill=color)167    return image168169def rect_svg(cell, scale: int = 1, start: int = 0) -> str:170    inner = cell._cell if hasattr(cell, '_cell') else cell171    if not is_hex(inner):172        raise MrlyError("Cell must be a hexagon.")173    tiled_cell = tile(inner, 3, 3)174    tile_h, tile_w = inner.types.shape175    orientation = get_orientation(tile_w, tile_h)176    match orientation:177        case Orientation.HORIZONTAL:178            dx = (3 * (tile_w + 1)) // 4179            dy = tile_h180            row_shift = tile_h // 2181            geom_crop_w = 2 * dx182            geom_crop_h = 2 * dy183            start_geom_x = (tile_w + 1) // 2184            start_geom_y = tile_h185        case Orientation.VERTICAL:186            dx = tile_w187            dy = (3 * (tile_h + 1)) // 4188            row_shift = tile_w // 2189            geom_crop_w = 2 * dx190            geom_crop_h = 2 * dy191            start_geom_x = tile_w192            start_geom_y = (tile_h + 1) // 2193    triangles = get_triangles(tiled_cell, start)194    img_width = geom_crop_w * scale195    img_height = geom_crop_h * scale196    offset_x = -start_geom_x197    offset_y = -start_geom_y198    elements = [f'<svg width="{int(img_width)}" height="{int(img_height)}" viewBox="0 0 {int(img_width)} {int(img_height)}" xmlns="http://www.w3.org/2000/svg">']199    for points, (r, g, b, a) in triangles:200        color = Color.rgba_to_hex(r, g, b, a)201        pts_str_list = []202        for p in points:203            px = int((p[0] + offset_x) * scale)204            py = int((p[1] + offset_y) * scale)205            pts_str_list.append(f"{px},{py}")206        pts_str = " ".join(pts_str_list)207        elements.append(f'<polygon points="{pts_str}" fill="{color}" stroke="none"/>')208    elements.append('</svg>')209    return "\n".join(elements)