diff --git a/libs/utils.py b/libs/utils.py index 3be8e7b..79e1ab4 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -144,232 +144,269 @@ class OutlinedText: }) def __post_init__(self): + # Cache for rotated characters + self._rotation_cache = {} + # Cache for character measurements + self._char_size_cache = {} self.texture = self._create_texture() + def _get_char_size(self, char): + """Cache character size measurements""" + if char not in self._char_size_cache: + if char in self.vertical_chars: + # For vertical chars, width and height are swapped + self._char_size_cache[char] = ray.Vector2(self.font_size, self.font_size) + else: + self._char_size_cache[char] = ray.measure_text_ex(self.font, char, self.font_size, 1.0) + return self._char_size_cache[char] + def _calculate_vertical_spacing(self, current_char, next_char=None): - # Check if current char is lowercase or whitespace - is_spacing_char = current_char.islower() or current_char.isspace() or current_char in self.no_space_chars + """Calculate vertical spacing between characters""" + # Check if current char is lowercase, whitespace or a special character + is_spacing_char = (current_char.islower() or + current_char.isspace() or + current_char in self.no_space_chars) # Additional check for capitalization transition - if next_char and current_char.isupper() and next_char.islower() or next_char in self.no_space_chars: + if next_char and ((current_char.isupper() and next_char.islower()) or + next_char in self.no_space_chars): is_spacing_char = True # Apply spacing factor if it's a spacing character - if is_spacing_char: - return self.font_size * (self.line_spacing * self.lowercase_spacing_factor) - return self.font_size * self.line_spacing + spacing = self.line_spacing * (self.lowercase_spacing_factor if is_spacing_char else 1.0) + return self.font_size * spacing - def _draw_rotated_char(self, image, font, char, pos, font_size, color, is_outline=False): - # Calculate character size - char_size = ray.measure_text_ex(font, char, font_size, 1.0) + def _get_rotated_char(self, char, color): + """Get or create a rotated character texture from cache""" + cache_key = (char, color[0], color[1], color[2], color[3]) - # Create a temporary image for the rotated character - temp_image = ray.gen_image_color(int(char_size.y), int(char_size.x), ray.Color(0, 0, 0, 0)) + if cache_key in self._rotation_cache: + return self._rotation_cache[cache_key] - # Draw the character on the temporary image + char_size = self._get_char_size(char) + + # For rotated text, we need extra padding to prevent cutoff + padding = max(int(self.font_size * 0.2), 2) # Add padding proportional to font size + temp_width = int(char_size.y) + padding * 2 + temp_height = int(char_size.x) + padding * 2 + + # Create a temporary image with padding to ensure characters aren't cut off + temp_image = ray.gen_image_color(temp_width, temp_height, ray.Color(0, 0, 0, 0)) + + # Calculate centering offsets + x_offset = padding + y_offset = padding + + # Draw the character centered in the temporary image ray.image_draw_text_ex( temp_image, - font, + self.font, char, - ray.Vector2(0, 0), - font_size, + ray.Vector2(x_offset-5, y_offset), + self.font_size, 1.0, color ) - # Rotate the temporary image 90 degrees - rotated_image = ray.gen_image_color(int(char_size.x), int(char_size.y), ray.Color(0, 0, 0, 0)) - for x in range(int(char_size.y)): - for y in range(int(char_size.x)): - pixel = ray.get_image_color(temp_image, y, int(char_size.y) - x - 1) - ray.image_draw_pixel( - rotated_image, - x, - y, - pixel - ) + # Rotate the temporary image 90 degrees counterclockwise + rotated_image = ray.gen_image_color(temp_height, temp_width, ray.Color(0, 0, 0, 0)) + for x in range(temp_width): + for y in range(temp_height): + pixel = ray.get_image_color(temp_image, x, temp_height - y - 1) + ray.image_draw_pixel(rotated_image, y, x, pixel) # Unload temporary image ray.unload_image(temp_image) - # Draw the rotated image - ray.image_draw( - image, - rotated_image, - ray.Rectangle(0, 0, rotated_image.width, rotated_image.height), - ray.Rectangle(int(pos.x), int(pos.y), rotated_image.width, rotated_image.height), - ray.WHITE - ) + # Cache the rotated image + self._rotation_cache[cache_key] = rotated_image + return rotated_image - # Unload rotated image - ray.unload_image(rotated_image) - - def _create_texture(self): - # Measure text size - text_size = ray.measure_text_ex(self.font, self.text, self.font_size, 1.0) - - # Determine dimensions based on orientation + def _calculate_dimensions(self): + """Calculate dimensions based on orientation""" if not self.vertical: - width = int(text_size.x + self.outline_thickness * 4) - height = int(text_size.y + self.outline_thickness * 4) - padding_x, padding_y = self.outline_thickness * 2, self.outline_thickness * 2 - else: - # For vertical text, calculate total height and max character width - char_heights = [ - self._calculate_vertical_spacing( - self.text[i], - self.text[i+1] if i+1 < len(self.text) else None - ) - for i in range(len(self.text)) - ] + # Horizontal text + text_size = ray.measure_text_ex(self.font, self.text, self.font_size, 1.0) - # Calculate the maximum character width (including outline) + # Add extra padding to prevent cutoff + extra_padding = max(int(self.font_size * 0.15), 2) + width = int(text_size.x + self.outline_thickness * 4 + extra_padding * 2) + height = int(text_size.y + self.outline_thickness * 4 + extra_padding * 2) + padding_x = self.outline_thickness * 2 + extra_padding + padding_y = self.outline_thickness * 2 + extra_padding + + return width, height, padding_x, padding_y + else: + # For vertical text, pre-calculate all character heights and widths + char_heights = [] char_widths = [] - for char in self.text: + + for i, char in enumerate(self.text): + next_char = self.text[i+1] if i+1 < len(self.text) else None + char_heights.append(self._calculate_vertical_spacing(char, next_char)) + + # For vertical characters, consider rotated dimensions if char in self.vertical_chars: - # For vertically drawn characters, use font size as width - char_width = self.font_size + # Use padded width for rotated characters + padding = max(int(self.font_size * 0.2), 2) * 2 + char_widths.append(self._get_char_size(char).x + padding) else: - # Normal character width - char_width = ray.measure_text_ex(self.font, char, self.font_size, 1.0).x - char_widths.append(char_width) + char_widths.append(self._get_char_size(char).x) max_char_width = max(char_widths) if char_widths else 0 total_height = sum(char_heights) if char_heights else 0 - # Adjust dimensions to be tighter around the text - width = int(max_char_width + self.outline_thickness * 2) # Reduced padding - height = int(total_height + self.outline_thickness * 2) # Reduced padding - padding_x = self.outline_thickness - padding_y = self.outline_thickness + # Add extra padding for vertical text + extra_padding = max(int(self.font_size * 0.15), 2) + width = int(max_char_width + self.outline_thickness * 4 + extra_padding * 2) + height = int(total_height + self.outline_thickness * 4 + extra_padding * 2) + padding_x = self.outline_thickness * 2 + extra_padding + padding_y = self.outline_thickness * 2 + extra_padding + + return width, height, padding_x, padding_y + + def _draw_horizontal_text(self, image, padding_x, padding_y): + """Draw horizontal text with outline""" + # Draw outline + for dx in range(-self.outline_thickness, self.outline_thickness + 1): + for dy in range(-self.outline_thickness, self.outline_thickness + 1): + if dx == 0 and dy == 0: + continue + ray.image_draw_text_ex( + image, + self.font, + self.text, + ray.Vector2(padding_x + dx, padding_y + dy), + self.font_size, + 1.0, + self.outline_color + ) + + # Draw main text + ray.image_draw_text_ex( + image, + self.font, + self.text, + ray.Vector2(padding_x, padding_y), + self.font_size, + 1.0, + self.text_color + ) + + def _draw_vertical_text(self, image, width, padding_x, padding_y): + """Draw vertical text with outline""" + # Precalculate positions and spacings to avoid redundant calculations + positions = [] + current_y = padding_y + + for i, char in enumerate(self.text): + char_size = self._get_char_size(char) + char_height = self._calculate_vertical_spacing( + char, + self.text[i+1] if i+1 < len(self.text) else None + ) + + # Calculate center position for each character + if char in self.vertical_chars: + # For vertical characters, we need to use the rotated image dimensions + rotated_img = self._get_rotated_char(char, self.text_color) + char_width = rotated_img.width + center_offset = (width - char_width) // 2 + else: + char_width = char_size.x + center_offset = (width - char_width) // 2 + + positions.append((char, center_offset, current_y, char_height, char in self.vertical_chars)) + current_y += char_height + + # First draw all outlines + for dx in range(-self.outline_thickness, self.outline_thickness + 1): + for dy in range(-self.outline_thickness, self.outline_thickness + 1): + if dx == 0 and dy == 0: + continue + + for char, center_offset, y_pos, _, is_vertical in positions: + if is_vertical: + rotated_img = self._get_rotated_char(char, self.outline_color) + ray.image_draw( + image, + rotated_img, + ray.Rectangle(0, 0, rotated_img.width, rotated_img.height), + ray.Rectangle( + int(center_offset + dx), + int(y_pos + dy), + rotated_img.width, + rotated_img.height + ), + ray.WHITE + ) + else: + ray.image_draw_text_ex( + image, + self.font, + char, + ray.Vector2(center_offset + dx, y_pos + dy), + self.font_size, + 1.0, + self.outline_color + ) + + # Then draw all main text + for char, center_offset, y_pos, _, is_vertical in positions: + if is_vertical: + rotated_img = self._get_rotated_char(char, self.text_color) + ray.image_draw( + image, + rotated_img, + ray.Rectangle(0, 0, rotated_img.width, rotated_img.height), + ray.Rectangle( + int(center_offset), + int(y_pos), + rotated_img.width, + rotated_img.height + ), + ray.WHITE + ) + else: + ray.image_draw_text_ex( + image, + self.font, + char, + ray.Vector2(center_offset, y_pos), + self.font_size, + 1.0, + self.text_color + ) + + def _create_texture(self): + """Create a texture with outlined text""" + # Calculate dimensions + width, height, padding_x, padding_y = self._calculate_dimensions() # Create transparent image image = ray.gen_image_color(width, height, ray.Color(0, 0, 0, 0)) - # Draw outline + # Draw text based on orientation if not self.vertical: - # Horizontal text outline - for dx in range(-self.outline_thickness, self.outline_thickness + 1): - for dy in range(-self.outline_thickness, self.outline_thickness + 1): - if dx == 0 and dy == 0: - continue - ray.image_draw_text_ex( - image, - self.font, - self.text, - ray.Vector2(padding_x + dx, padding_y + dy), - self.font_size, - 1.0, - self.outline_color - ) + self._draw_horizontal_text(image, padding_x, padding_y) else: - # Vertical text outline - current_y = padding_y - for dx in range(-self.outline_thickness, self.outline_thickness + 1): - for dy in range(-self.outline_thickness, self.outline_thickness + 1): - if dx == 0 and dy == 0: - continue + self._draw_vertical_text(image, width, padding_x, padding_y) - current_y = padding_y - for i, char in enumerate(self.text): - if char in self.vertical_chars: - char_width = self.font_size - else: - char_width = ray.measure_text_ex(self.font, char, self.font_size, 1.0).x - - # Calculate centered position - center_offset = (width - char_width) // 2 - char_height = self._calculate_vertical_spacing( - char, - self.text[i+1] if i+1 < len(self.text) else None - ) - - if char in self.vertical_chars: - self._draw_rotated_char( - image, - self.font, - char, - ray.Vector2( - center_offset + dx, - current_y + dy - ), - self.font_size, - self.outline_color, - is_outline=True - ) - else: - ray.image_draw_text_ex( - image, - self.font, - char, - ray.Vector2(center_offset + dx, current_y + dy), - self.font_size, - 1.0, - self.outline_color - ) - - current_y += char_height - - # Draw main text - if not self.vertical: - # Horizontal text - ray.image_draw_text_ex( - image, - self.font, - self.text, - ray.Vector2(padding_x, padding_y), - self.font_size, - 1.0, - self.text_color - ) - else: - # Vertical text - current_y = padding_y - for i, char in enumerate(self.text): - if char in self.vertical_chars: - char_width = self.font_size - else: - char_width = ray.measure_text_ex(self.font, char, self.font_size, 1.0).x - - # Calculate centered position - center_offset = (width - char_width) // 2 - char_height = self._calculate_vertical_spacing( - char, - self.text[i+1] if i+1 < len(self.text) else None - ) - - if char in self.vertical_chars: - self._draw_rotated_char( - image, - self.font, - char, - ray.Vector2( - center_offset, - current_y - ), - self.font_size, - self.text_color - ) - else: - ray.image_draw_text_ex( - image, - self.font, - char, - ray.Vector2(center_offset, current_y), - self.font_size, - 1.0, - self.text_color - ) - - current_y += char_height - - # Create texture and clean up + # Create texture from image texture = ray.load_texture_from_image(image) ray.unload_image(image) return texture def draw(self, src: ray.Rectangle, dest: ray.Rectangle, origin: ray.Vector2, rotation: float, color: ray.Color): + """Draw the outlined text""" ray.draw_texture_pro(self.texture, src, dest, origin, rotation, color) def unload(self): + """Clean up resources""" + # Unload all cached rotated images + for img in self._rotation_cache.values(): + ray.unload_image(img) + self._rotation_cache.clear() + + # Unload texture ray.unload_texture(self.texture)