mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 11:40:13 +01:00
This commit adds a new `restart()` method to animation classes to properly reset their state, avoiding the need to recreate animations. It also adds several new background variations and improves background selection.
830 lines
33 KiB
Python
830 lines
33 KiB
Python
import hashlib
|
||
import math
|
||
import os
|
||
import tempfile
|
||
import time
|
||
import zipfile
|
||
from dataclasses import dataclass, field
|
||
from functools import lru_cache
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
import pyray as ray
|
||
import tomlkit
|
||
|
||
#TJA Format creator is unknown. I did not create the format, but I did write the parser though.
|
||
|
||
def get_zip_filenames(zip_path: Path) -> list[str]:
|
||
result = []
|
||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||
file_list = zip_ref.namelist()
|
||
for file_name in file_list:
|
||
result.append(file_name)
|
||
return result
|
||
|
||
def load_image_from_zip(zip_path: Path, filename: str) -> ray.Image:
|
||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||
with zip_ref.open(filename) as image_file:
|
||
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
|
||
temp_file.write(image_file.read())
|
||
temp_file_path = temp_file.name
|
||
image = ray.load_image(temp_file_path)
|
||
os.remove(temp_file_path)
|
||
return image
|
||
|
||
def load_texture_from_zip(zip_path: Path, filename: str) -> ray.Texture:
|
||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||
with zip_ref.open(filename) as image_file:
|
||
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
|
||
temp_file.write(image_file.read())
|
||
temp_file_path = temp_file.name
|
||
texture = ray.load_texture(temp_file_path)
|
||
os.remove(temp_file_path)
|
||
return texture
|
||
|
||
def load_all_textures_from_zip(zip_path: Path) -> dict[str, list[ray.Texture]]:
|
||
result_dict = dict()
|
||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||
files = zip_ref.namelist()
|
||
for file in files:
|
||
with zip_ref.open(file) as image_file:
|
||
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
|
||
temp_file.write(image_file.read())
|
||
temp_file_path = temp_file.name
|
||
texture = ray.load_texture(temp_file_path)
|
||
os.remove(temp_file_path)
|
||
|
||
true_filename, index = file.split('_img')
|
||
index = int(index.split('.')[0])
|
||
if true_filename not in result_dict:
|
||
result_dict[true_filename] = []
|
||
while len(result_dict[true_filename]) <= index:
|
||
result_dict[true_filename].append(None)
|
||
result_dict[true_filename][index] = texture
|
||
return result_dict
|
||
|
||
def rounded(num: float) -> int:
|
||
sign = 1 if (num >= 0) else -1
|
||
num = abs(num)
|
||
result = int(num)
|
||
if (num - result >= 0.5):
|
||
result += 1
|
||
return sign * result
|
||
|
||
def get_current_ms() -> int:
|
||
return rounded(time.time() * 1000)
|
||
|
||
def strip_comments(code: str) -> str:
|
||
result = ''
|
||
index = 0
|
||
for line in code.splitlines():
|
||
comment_index = line.find('//')
|
||
if comment_index == -1:
|
||
result += line
|
||
elif comment_index != 0 and not line[:comment_index].isspace():
|
||
result += line[:comment_index]
|
||
index += 1
|
||
return result
|
||
|
||
@lru_cache
|
||
def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) -> float:
|
||
if bpm == 0:
|
||
return 0
|
||
beat_duration = 60 / bpm
|
||
total_time = time_signature * beat_duration
|
||
total_frames = 60 * total_time
|
||
return (distance / total_frames)
|
||
|
||
def get_config() -> dict[str, Any]:
|
||
if Path('dev-config.toml').exists():
|
||
with open(Path('dev-config.toml'), "r", encoding="utf-8") as f:
|
||
config_file = tomlkit.load(f)
|
||
return config_file
|
||
with open(Path('config.toml'), "r", encoding="utf-8") as f:
|
||
config_file = tomlkit.load(f)
|
||
return config_file
|
||
|
||
def save_config(config: dict[str, Any]) -> None:
|
||
if Path('dev-config.toml').exists():
|
||
with open(Path('dev-config.toml'), "w", encoding="utf-8") as f:
|
||
tomlkit.dump(config, f)
|
||
return
|
||
with open(Path('config.toml'), "w", encoding="utf-8") as f:
|
||
tomlkit.dump(config, f)
|
||
|
||
def is_l_don_pressed() -> bool:
|
||
keys = global_data.config["keys"]["left_don"]
|
||
gamepad_buttons = global_data.config["gamepad"]["left_don"]
|
||
for key in keys:
|
||
if ray.is_key_pressed(ord(key)):
|
||
return True
|
||
|
||
if ray.is_gamepad_available(0):
|
||
for button in gamepad_buttons:
|
||
if ray.is_gamepad_button_pressed(0, button):
|
||
return True
|
||
|
||
mid_x, mid_y = (1280//2, 720)
|
||
allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP}
|
||
if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()):
|
||
for i in range(min(ray.get_touch_point_count(), 10)):
|
||
tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y)
|
||
if math.dist(tap_pos, (mid_x, mid_y)) < 300 and tap_pos[0] <= mid_x:
|
||
return True
|
||
|
||
return False
|
||
|
||
def is_r_don_pressed() -> bool:
|
||
keys = global_data.config["keys"]["right_don"]
|
||
gamepad_buttons = global_data.config["gamepad"]["right_don"]
|
||
for key in keys:
|
||
if ray.is_key_pressed(ord(key)):
|
||
return True
|
||
|
||
if ray.is_gamepad_available(0):
|
||
for button in gamepad_buttons:
|
||
if ray.is_gamepad_button_pressed(0, button):
|
||
return True
|
||
|
||
mid_x, mid_y = (1280//2, 720)
|
||
allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP}
|
||
if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()):
|
||
for i in range(min(ray.get_touch_point_count(), 10)):
|
||
tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y)
|
||
if math.dist(tap_pos, (mid_x, mid_y)) < 300 and tap_pos[0] > mid_x:
|
||
return True
|
||
|
||
return False
|
||
|
||
def is_l_kat_pressed() -> bool:
|
||
keys = global_data.config["keys"]["left_kat"]
|
||
gamepad_buttons = global_data.config["gamepad"]["left_kat"]
|
||
for key in keys:
|
||
if ray.is_key_pressed(ord(key)):
|
||
return True
|
||
|
||
if ray.is_gamepad_available(0):
|
||
for button in gamepad_buttons:
|
||
if ray.is_gamepad_button_pressed(0, button):
|
||
return True
|
||
|
||
mid_x, mid_y = (1280//2, 720)
|
||
allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP}
|
||
if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()):
|
||
for i in range(min(ray.get_touch_point_count(), 10)):
|
||
tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y)
|
||
if math.dist(tap_pos, (mid_x, mid_y)) >= 300 and tap_pos[0] <= mid_x:
|
||
return True
|
||
|
||
return False
|
||
|
||
def is_r_kat_pressed() -> bool:
|
||
keys = global_data.config["keys"]["right_kat"]
|
||
gamepad_buttons = global_data.config["gamepad"]["right_kat"]
|
||
for key in keys:
|
||
if ray.is_key_pressed(ord(key)):
|
||
return True
|
||
|
||
if ray.is_gamepad_available(0):
|
||
for button in gamepad_buttons:
|
||
if ray.is_gamepad_button_pressed(0, button):
|
||
return True
|
||
|
||
mid_x, mid_y = (1280//2, 720)
|
||
allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP}
|
||
if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()):
|
||
for i in range(min(ray.get_touch_point_count(), 10)):
|
||
tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y)
|
||
if math.dist(tap_pos, (mid_x, mid_y)) >= 300 and tap_pos[0] > mid_x:
|
||
return True
|
||
|
||
return False
|
||
|
||
def draw_scaled_texture(texture: ray.Texture, x: int, y: int, scale: float, color: ray.Color) -> None:
|
||
src_rect = ray.Rectangle(0, 0, texture.width, texture.height)
|
||
dst_rect = ray.Rectangle(x, y, texture.width*scale, texture.height*scale)
|
||
ray.draw_texture_pro(texture, src_rect, dst_rect, ray.Vector2(0, 0), 0, color)
|
||
|
||
@dataclass
|
||
class SessionData:
|
||
selected_difficulty: int = 0
|
||
song_title: str = ''
|
||
result_score: int = 0
|
||
result_good: int = 0
|
||
result_ok: int = 0
|
||
result_bad: int = 0
|
||
result_max_combo: int = 0
|
||
result_total_drumroll: int = 0
|
||
result_gauge_length: int = 0
|
||
|
||
session_data = SessionData()
|
||
|
||
def reset_session():
|
||
return SessionData()
|
||
|
||
@dataclass
|
||
class GlobalData:
|
||
selected_song: Path = Path()
|
||
textures: dict[str, list[ray.Texture]] = field(default_factory=lambda: dict())
|
||
songs_played: int = 0
|
||
config: dict = field(default_factory=lambda: dict())
|
||
|
||
global_data = GlobalData()
|
||
|
||
rotation_cache = dict()
|
||
char_size_cache = dict()
|
||
horizontal_cache = dict()
|
||
text_cache = set()
|
||
if not Path('cache/image').exists():
|
||
Path('cache').mkdir()
|
||
Path('cache/image').mkdir()
|
||
for file in Path('cache/image').iterdir():
|
||
text_cache.add(file.stem)
|
||
|
||
@dataclass
|
||
class OutlinedText:
|
||
text: str
|
||
font_size: int
|
||
text_color: ray.Color
|
||
outline_color: ray.Color
|
||
font: ray.Font = ray.Font()
|
||
outline_thickness: int = 2
|
||
vertical: bool = False
|
||
line_spacing: float = 1.0 # Line spacing for vertical text
|
||
horizontal_spacing: float = 1.0 # Character spacing for horizontal text
|
||
lowercase_spacing_factor: float = 0.85 # Adjust spacing for lowercase letters and whitespace
|
||
vertical_chars: set = field(default_factory=lambda: {'-', '‐', '|', '/', '\\', 'ー', '~', '~', '(', ')', '(', ')',
|
||
'「', '」', '[', ']', '[', ']', '【', '】', '…', '→', '→', ':', ':'})
|
||
no_space_chars: set = field(default_factory=lambda: {
|
||
'ぁ', 'ア','ぃ', 'イ','ぅ', 'ウ','ぇ', 'エ','ぉ', 'オ',
|
||
'ゃ', 'ャ','ゅ', 'ュ','ょ', 'ョ','っ', 'ッ','ゎ', 'ヮ',
|
||
'ヶ', 'ヵ','ㇰ','ㇱ','ㇲ','ㇳ','ㇴ','ㇵ','ㇶ','ㇷ','ㇸ',
|
||
'ㇹ','ㇺ','ㇻ','ㇼ','ㇽ','ㇾ','ㇿ'
|
||
})
|
||
# New field for horizontal exception strings
|
||
horizontal_exceptions: set = field(default_factory=lambda: {'!!!!', '!!!', '!!', '!!','!!!','!?', '!?', '??', '??', '†††', '(°∀°)', '(°∀°)'})
|
||
# New field for adjacent punctuation characters
|
||
adjacent_punctuation: set = field(default_factory=lambda: {'.', ',', '。', '、', "'", '"', '´', '`'})
|
||
|
||
def __post_init__(self):
|
||
# Cache for rotated characters
|
||
self._rotation_cache = rotation_cache
|
||
# Cache for character measurements
|
||
self._char_size_cache = char_size_cache
|
||
# Cache for horizontal exception measurements
|
||
self._horizontal_cache = horizontal_cache
|
||
self.hash = self._get_hash()
|
||
self.texture = self._create_texture()
|
||
|
||
def _load_font_for_text(self, text: str) -> ray.Font:
|
||
codepoint_count = ray.ffi.new('int *', 0)
|
||
unique_codepoints = set(text)
|
||
codepoints = ray.load_codepoints(''.join(unique_codepoints), codepoint_count)
|
||
return ray.load_font_ex(str(Path('Graphics/Modified-DFPKanteiryu-XB.ttf')), self.font_size, codepoints, 0)
|
||
|
||
def _get_hash(self):
|
||
n = hashlib.sha256()
|
||
n.update(self.text.encode('utf-8'))
|
||
n.update(str(self.vertical).encode('utf-8'))
|
||
n.update(str(self.horizontal_spacing).encode('utf-8')) # Include horizontal spacing in hash
|
||
n.update(str(self.outline_color.a).encode('utf-8'))
|
||
n.update(str(self.outline_color.r).encode('utf-8'))
|
||
n.update(str(self.outline_color.g).encode('utf-8'))
|
||
n.update(str(self.outline_color.b).encode('utf-8'))
|
||
n.update(str(self.text_color.a).encode('utf-8'))
|
||
n.update(str(self.text_color.r).encode('utf-8'))
|
||
n.update(str(self.text_color.g).encode('utf-8'))
|
||
n.update(str(self.text_color.b).encode('utf-8'))
|
||
n.update(str(self.font_size).encode('utf-8'))
|
||
return n.hexdigest()
|
||
|
||
def _parse_text_segments(self):
|
||
"""Parse text into segments, identifying horizontal exceptions"""
|
||
if not self.vertical:
|
||
return [{'text': self.text, 'is_horizontal': False}]
|
||
|
||
segments = []
|
||
i = 0
|
||
current_segment = ""
|
||
|
||
while i < len(self.text):
|
||
# Check if any horizontal exception starts at current position
|
||
found_exception = None
|
||
for exception in self.horizontal_exceptions:
|
||
if self.text[i:].startswith(exception):
|
||
found_exception = exception
|
||
break
|
||
|
||
if found_exception:
|
||
# Save current segment if it exists
|
||
if current_segment:
|
||
segments.append({'text': current_segment, 'is_horizontal': False})
|
||
current_segment = ""
|
||
|
||
# Add horizontal exception as separate segment
|
||
segments.append({'text': found_exception, 'is_horizontal': True})
|
||
i += len(found_exception)
|
||
else:
|
||
# Add character to current segment
|
||
current_segment += self.text[i]
|
||
i += 1
|
||
|
||
# Add remaining segment
|
||
if current_segment:
|
||
segments.append({'text': current_segment, 'is_horizontal': False})
|
||
|
||
return segments
|
||
|
||
def _group_characters_with_punctuation(self, text):
|
||
"""Group characters with their adjacent punctuation"""
|
||
groups = []
|
||
i = 0
|
||
|
||
while i < len(text):
|
||
current_char = text[i]
|
||
group = {'main_char': current_char, 'adjacent_punct': []}
|
||
|
||
# Look ahead for adjacent punctuation
|
||
j = i + 1
|
||
while j < len(text) and text[j] in self.adjacent_punctuation:
|
||
group['adjacent_punct'].append(text[j])
|
||
j += 1
|
||
|
||
groups.append(group)
|
||
i = j # Move to next non-punctuation character
|
||
|
||
return groups
|
||
|
||
def _get_horizontal_exception_texture(self, text: str, color):
|
||
"""Get or create a texture for horizontal exception text"""
|
||
cache_key = (text, color.r, color.g, color.b, color.a, 'horizontal')
|
||
|
||
if cache_key in self._horizontal_cache:
|
||
return self._horizontal_cache[cache_key]
|
||
|
||
# Measure the text
|
||
text_size = ray.measure_text_ex(self.font, text, self.font_size, 1.0)
|
||
padding = int(self.outline_thickness * 3)
|
||
|
||
# Create image with proper dimensions
|
||
img_width = int(text_size.x + padding * 2)
|
||
img_height = int(text_size.y + padding * 2)
|
||
temp_image = ray.gen_image_color(img_width, img_height, ray.Color(0, 0, 0, 0))
|
||
|
||
# Draw the text centered
|
||
ray.image_draw_text_ex(
|
||
temp_image,
|
||
self.font,
|
||
text,
|
||
ray.Vector2(padding, padding),
|
||
self.font_size,
|
||
1.0,
|
||
color
|
||
)
|
||
|
||
# Cache the image
|
||
self._horizontal_cache[cache_key] = temp_image
|
||
return temp_image
|
||
|
||
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):
|
||
"""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())
|
||
|
||
# Additional check for capitalization transition
|
||
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
|
||
spacing = self.line_spacing * (self.lowercase_spacing_factor if is_spacing_char else 1.0)
|
||
return self.font_size * spacing
|
||
|
||
def _get_rotated_char(self, char: str, color):
|
||
"""Get or create a rotated character texture from cache"""
|
||
cache_key = (char, color.r, color.g, color.b, color.a)
|
||
|
||
if cache_key in self._rotation_cache:
|
||
return self._rotation_cache[cache_key]
|
||
|
||
char_size = self._get_char_size(char)
|
||
padding = int(self.outline_thickness * 3) # Increased padding
|
||
temp_width = max(int(char_size.y) + padding, self.font_size + padding)
|
||
temp_height = max(int(char_size.x) + padding, self.font_size + padding)
|
||
temp_image = ray.gen_image_color(temp_width, temp_height, ray.Color(0, 0, 0, 0))
|
||
|
||
center_x = (temp_width - char_size.y) // 2
|
||
center_y = (temp_height - char_size.x) // 2
|
||
|
||
ray.image_draw_text_ex(
|
||
temp_image,
|
||
self.font,
|
||
char,
|
||
ray.Vector2(center_x-5, center_y), # Centered placement with padding
|
||
self.font_size,
|
||
1.0,
|
||
color
|
||
)
|
||
|
||
# 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)
|
||
|
||
# Cache the rotated image
|
||
self._rotation_cache[cache_key] = rotated_image
|
||
return rotated_image
|
||
|
||
def _calculate_horizontal_text_width(self):
|
||
"""Calculate the total width of horizontal text with custom spacing"""
|
||
if not self.text:
|
||
return 0
|
||
|
||
total_width = 0
|
||
for i, char in enumerate(self.text):
|
||
char_size = ray.measure_text_ex(self.font, char, self.font_size, 1.0)
|
||
total_width += char_size.x
|
||
|
||
# Add spacing between characters (except for the last character)
|
||
if i < len(self.text) - 1:
|
||
total_width += (char_size.x * (self.horizontal_spacing - 1.0))
|
||
|
||
return total_width
|
||
|
||
def _calculate_dimensions(self):
|
||
padding = int(self.outline_thickness * 3)
|
||
|
||
if not self.vertical:
|
||
if self.horizontal_spacing == 1.0:
|
||
# Use default raylib measurement for normal spacing
|
||
text_size = ray.measure_text_ex(self.font, self.text, self.font_size, 1.0)
|
||
return int(text_size.x + padding * 2), int(text_size.y + padding * 2)
|
||
else:
|
||
# Calculate custom spacing width
|
||
text_width = self._calculate_horizontal_text_width()
|
||
text_height = ray.measure_text_ex(self.font, "Ag", self.font_size, 1.0).y # Use sample chars for height
|
||
return int(text_width + padding * 2), int(text_height + padding * 2)
|
||
else:
|
||
# Parse text into segments
|
||
segments = self._parse_text_segments()
|
||
|
||
char_heights = []
|
||
char_widths = []
|
||
|
||
for segment in segments:
|
||
if segment['is_horizontal']:
|
||
# For horizontal exceptions, add their height as spacing
|
||
text_size = ray.measure_text_ex(self.font, segment['text'], self.font_size, 1.0)
|
||
char_heights.append(text_size.y * self.line_spacing)
|
||
char_widths.append(text_size.x)
|
||
else:
|
||
# Process vertical text with character grouping
|
||
char_groups = self._group_characters_with_punctuation(segment['text'])
|
||
|
||
for i, group in enumerate(char_groups):
|
||
main_char = group['main_char']
|
||
adjacent_punct = group['adjacent_punct']
|
||
|
||
# Get next group's main character for spacing calculation
|
||
next_char = char_groups[i+1]['main_char'] if i+1 < len(char_groups) else None
|
||
char_heights.append(self._calculate_vertical_spacing(main_char, next_char))
|
||
|
||
# Calculate width considering main char + adjacent punctuation
|
||
main_char_size = self._get_char_size(main_char)
|
||
group_width = main_char_size.x
|
||
|
||
# Add width for adjacent punctuation
|
||
for punct in adjacent_punct:
|
||
punct_size = self._get_char_size(punct)
|
||
group_width += punct_size.x
|
||
|
||
# For vertical characters, consider rotated dimensions
|
||
if main_char in self.vertical_chars:
|
||
char_widths.append(group_width + padding)
|
||
else:
|
||
char_widths.append(group_width)
|
||
|
||
max_char_width = max(char_widths) if char_widths else 0
|
||
total_height = sum(char_heights) if char_heights else 0
|
||
|
||
width = int(max_char_width + padding * 2) # Padding on both sides
|
||
height = int(total_height + padding * 2) # Padding on top and bottom
|
||
|
||
return width, height
|
||
|
||
def _draw_horizontal_text(self, image):
|
||
if self.horizontal_spacing == 1.0:
|
||
# Use original method for normal spacing
|
||
text_size = ray.measure_text_ex(self.font, self.text, self.font_size, 1.0)
|
||
position = ray.Vector2((image.width - text_size.x) / 2, (image.height - text_size.y) / 2)
|
||
|
||
for dx in range(-self.outline_thickness, self.outline_thickness + 1):
|
||
for dy in range(-self.outline_thickness, self.outline_thickness + 1):
|
||
# Skip the center position (will be drawn as main text)
|
||
if dx == 0 and dy == 0:
|
||
continue
|
||
|
||
# Calculate outline distance
|
||
dist = (dx*dx + dy*dy) ** 0.5
|
||
|
||
# Only draw outline positions that are near the outline thickness
|
||
if dist <= self.outline_thickness + 0.5:
|
||
ray.image_draw_text_ex(
|
||
image,
|
||
self.font,
|
||
self.text,
|
||
ray.Vector2(position.x + dx, position.y + dy),
|
||
self.font_size,
|
||
1.0,
|
||
self.outline_color
|
||
)
|
||
|
||
# Draw main text
|
||
ray.image_draw_text_ex(
|
||
image,
|
||
self.font,
|
||
self.text,
|
||
position,
|
||
self.font_size,
|
||
1.0,
|
||
self.text_color
|
||
)
|
||
else:
|
||
# Draw text with custom character spacing
|
||
text_width = self._calculate_horizontal_text_width()
|
||
text_height = ray.measure_text_ex(self.font, "Ag", self.font_size, 1.0).y
|
||
|
||
start_x = (image.width - text_width) / 2
|
||
start_y = (image.height - text_height) / 2
|
||
|
||
# First draw all outlines
|
||
current_x = start_x
|
||
for i, char in enumerate(self.text):
|
||
char_size = ray.measure_text_ex(self.font, char, self.font_size, 1.0)
|
||
|
||
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
|
||
|
||
dist = (dx*dx + dy*dy) ** 0.5
|
||
if dist <= self.outline_thickness + 0.5:
|
||
ray.image_draw_text_ex(
|
||
image,
|
||
self.font,
|
||
char,
|
||
ray.Vector2(current_x + dx, start_y + dy),
|
||
self.font_size,
|
||
1.0,
|
||
self.outline_color
|
||
)
|
||
|
||
# Move to next character position
|
||
current_x += char_size.x
|
||
if i < len(self.text) - 1: # Add spacing except for last character
|
||
current_x += (char_size.x * (self.horizontal_spacing - 1.0))
|
||
|
||
# Then draw all main text
|
||
current_x = start_x
|
||
for i, char in enumerate(self.text):
|
||
char_size = ray.measure_text_ex(self.font, char, self.font_size, 1.0)
|
||
|
||
ray.image_draw_text_ex(
|
||
image,
|
||
self.font,
|
||
char,
|
||
ray.Vector2(current_x, start_y),
|
||
self.font_size,
|
||
1.0,
|
||
self.text_color
|
||
)
|
||
|
||
# Move to next character position
|
||
current_x += char_size.x
|
||
if i < len(self.text) - 1: # Add spacing except for last character
|
||
current_x += (char_size.x * (self.horizontal_spacing - 1.0))
|
||
|
||
def _draw_vertical_text(self, image, width):
|
||
padding = int(self.outline_thickness * 2)
|
||
segments = self._parse_text_segments()
|
||
|
||
positions = []
|
||
current_y = padding # Start with padding at the top
|
||
|
||
for segment in segments:
|
||
if segment['is_horizontal']:
|
||
# Handle horizontal exception
|
||
text_size = ray.measure_text_ex(self.font, segment['text'], self.font_size, 1.0)
|
||
center_offset = (width - text_size.x) // 2
|
||
char_height = text_size.y * self.line_spacing
|
||
|
||
positions.append({
|
||
'type': 'horizontal',
|
||
'text': segment['text'],
|
||
'x': center_offset,
|
||
'y': current_y,
|
||
'height': char_height
|
||
})
|
||
current_y += char_height
|
||
else:
|
||
# Handle vertical text with character grouping
|
||
char_groups = self._group_characters_with_punctuation(segment['text'])
|
||
|
||
for i, group in enumerate(char_groups):
|
||
main_char = group['main_char']
|
||
adjacent_punct = group['adjacent_punct']
|
||
|
||
# Get next group for spacing calculation
|
||
next_char = char_groups[i+1]['main_char'] if i+1 < len(char_groups) else None
|
||
char_height = self._calculate_vertical_spacing(main_char, next_char)
|
||
|
||
# Calculate positioning for main character
|
||
main_char_size = self._get_char_size(main_char)
|
||
|
||
if main_char in self.vertical_chars:
|
||
rotated_img = self._get_rotated_char(main_char, self.text_color)
|
||
main_char_width = rotated_img.width
|
||
center_offset = (width - main_char_width) // 2
|
||
else:
|
||
main_char_width = main_char_size.x
|
||
center_offset = (width - main_char_width) // 2
|
||
|
||
# Add main character position
|
||
positions.append({
|
||
'type': 'vertical',
|
||
'char': main_char,
|
||
'x': center_offset,
|
||
'y': current_y,
|
||
'height': char_height,
|
||
'is_vertical_char': main_char in self.vertical_chars
|
||
})
|
||
|
||
# Add adjacent punctuation positions
|
||
punct_x_offset = center_offset + main_char_width
|
||
for punct in adjacent_punct:
|
||
punct_size = self._get_char_size(punct)
|
||
|
||
positions.append({
|
||
'type': 'vertical',
|
||
'char': punct,
|
||
'x': punct_x_offset,
|
||
'y': current_y+5,
|
||
'height': 0, # No additional height for punctuation
|
||
'is_vertical_char': punct in self.vertical_chars,
|
||
'is_adjacent': True
|
||
})
|
||
|
||
punct_x_offset += punct_size.x
|
||
|
||
current_y += char_height
|
||
|
||
# First draw all outlines
|
||
outline_thickness = int(self.outline_thickness)
|
||
|
||
for pos in positions:
|
||
if pos['type'] == 'horizontal':
|
||
# Draw horizontal text outline
|
||
for dx in range(-outline_thickness, outline_thickness + 1):
|
||
for dy in range(-outline_thickness, outline_thickness + 1):
|
||
if dx == 0 and dy == 0:
|
||
continue
|
||
|
||
dist = (dx*dx + dy*dy) ** 0.5
|
||
if dist <= outline_thickness + 0.5:
|
||
ray.image_draw_text_ex(
|
||
image,
|
||
self.font,
|
||
pos['text'],
|
||
ray.Vector2(pos['x'] + dx, pos['y'] + dy),
|
||
self.font_size,
|
||
1.0,
|
||
self.outline_color
|
||
)
|
||
else:
|
||
# Draw vertical character outline
|
||
for dx in range(-outline_thickness, outline_thickness + 1):
|
||
for dy in range(-outline_thickness, outline_thickness + 1):
|
||
if dx == 0 and dy == 0:
|
||
continue
|
||
|
||
dist = (dx*dx + dy*dy) ** 0.5
|
||
if dist <= outline_thickness + 0.5:
|
||
if pos['is_vertical_char']:
|
||
rotated_img = self._get_rotated_char(pos['char'], self.outline_color)
|
||
ray.image_draw(
|
||
image,
|
||
rotated_img,
|
||
ray.Rectangle(0, 0, rotated_img.width, rotated_img.height),
|
||
ray.Rectangle(
|
||
int(pos['x'] + dx),
|
||
int(pos['y'] + dy),
|
||
rotated_img.width,
|
||
rotated_img.height
|
||
),
|
||
ray.WHITE
|
||
)
|
||
else:
|
||
ray.image_draw_text_ex(
|
||
image,
|
||
self.font,
|
||
pos['char'],
|
||
ray.Vector2(pos['x'] + dx, pos['y'] + dy),
|
||
self.font_size,
|
||
1.0,
|
||
self.outline_color
|
||
)
|
||
|
||
# Then draw all main text
|
||
for pos in positions:
|
||
if pos['type'] == 'horizontal':
|
||
# Draw horizontal text
|
||
ray.image_draw_text_ex(
|
||
image,
|
||
self.font,
|
||
pos['text'],
|
||
ray.Vector2(pos['x'], pos['y']),
|
||
self.font_size,
|
||
1.0,
|
||
self.text_color
|
||
)
|
||
else:
|
||
# Draw vertical character
|
||
if pos['is_vertical_char']:
|
||
rotated_img = self._get_rotated_char(pos['char'], self.text_color)
|
||
ray.image_draw(
|
||
image,
|
||
rotated_img,
|
||
ray.Rectangle(0, 0, rotated_img.width, rotated_img.height),
|
||
ray.Rectangle(
|
||
int(pos['x']),
|
||
int(pos['y']),
|
||
rotated_img.width,
|
||
rotated_img.height
|
||
),
|
||
ray.WHITE
|
||
)
|
||
else:
|
||
ray.image_draw_text_ex(
|
||
image,
|
||
self.font,
|
||
pos['char'],
|
||
ray.Vector2(pos['x'], pos['y']),
|
||
self.font_size,
|
||
1.0,
|
||
self.text_color
|
||
)
|
||
|
||
def _create_texture(self):
|
||
if self.hash in text_cache:
|
||
texture = ray.load_texture(f'cache/image/{self.hash}.png')
|
||
return texture
|
||
|
||
self.font = self._load_font_for_text(self.text)
|
||
|
||
width, height = self._calculate_dimensions()
|
||
|
||
width += int(self.outline_thickness * 1.5)
|
||
height += int(self.outline_thickness * 1.5)
|
||
|
||
image = ray.gen_image_color(width, height, ray.Color(0, 0, 0, 0))
|
||
|
||
if not self.vertical:
|
||
self._draw_horizontal_text(image)
|
||
else:
|
||
self._draw_vertical_text(image, width)
|
||
|
||
ray.export_image(image, f'cache/image/{self.hash}.png')
|
||
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):
|
||
ray.draw_texture_pro(self.texture, src, dest, origin, rotation, color)
|
||
|
||
def unload(self):
|
||
for img in self._rotation_cache.values():
|
||
ray.unload_image(img)
|
||
self._rotation_cache.clear()
|
||
|
||
for img in self._horizontal_cache.values():
|
||
ray.unload_image(img)
|
||
self._horizontal_cache.clear()
|
||
|
||
ray.unload_texture(self.texture)
|