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)