mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 03:30:13 +01:00
rewrote outlined text
This commit is contained in:
12
PyTaiko.py
12
PyTaiko.py
@@ -18,6 +18,7 @@ from libs.utils import (
|
||||
global_data,
|
||||
load_all_textures_from_zip,
|
||||
)
|
||||
from scenes.devtest import DevScreen
|
||||
from scenes.entry import EntryScreen
|
||||
from scenes.game import GameScreen
|
||||
from scenes.result import ResultScreen
|
||||
@@ -33,6 +34,7 @@ class Screens:
|
||||
GAME = "GAME"
|
||||
RESULT = "RESULT"
|
||||
SETTINGS = "SETTINGS"
|
||||
DEV_MENU = "DEV_MENU"
|
||||
|
||||
def create_song_db():
|
||||
with sqlite3.connect('scores.db') as con:
|
||||
@@ -84,24 +86,26 @@ def main():
|
||||
if global_data.config["video"]["fullscreen"]:
|
||||
ray.maximize_window()
|
||||
|
||||
current_screen = Screens.TITLE
|
||||
current_screen = Screens.DEV_MENU
|
||||
|
||||
audio.init_audio_device()
|
||||
|
||||
title_screen = TitleScreen(screen_width, screen_height)
|
||||
entry_screen = EntryScreen(screen_width, screen_height)
|
||||
song_select_screen = SongSelectScreen(screen_width, screen_height)
|
||||
#song_select_screen = SongSelectScreen(screen_width, screen_height)
|
||||
game_screen = GameScreen(screen_width, screen_height)
|
||||
result_screen = ResultScreen(screen_width, screen_height)
|
||||
settings_screen = SettingsScreen(screen_width, screen_height)
|
||||
dev_screen = DevScreen(screen_width, screen_height)
|
||||
|
||||
screen_mapping = {
|
||||
Screens.ENTRY: entry_screen,
|
||||
Screens.TITLE: title_screen,
|
||||
Screens.SONG_SELECT: song_select_screen,
|
||||
#Screens.SONG_SELECT: song_select_screen,
|
||||
Screens.GAME: game_screen,
|
||||
Screens.RESULT: result_screen,
|
||||
Screens.SETTINGS: settings_screen
|
||||
Screens.SETTINGS: settings_screen,
|
||||
Screens.DEV_MENU: dev_screen
|
||||
}
|
||||
target = ray.load_render_texture(screen_width, screen_height)
|
||||
ray.set_texture_filter(target.texture, ray.TextureFilter.TEXTURE_FILTER_TRILINEAR)
|
||||
|
||||
BIN
Songs/TRIPLE HELIX/TRIPLE HELIX.ogg
Normal file
BIN
Songs/TRIPLE HELIX/TRIPLE HELIX.ogg
Normal file
Binary file not shown.
1945
Songs/TRIPLE HELIX/TRIPLE HELIX.tja
Normal file
1945
Songs/TRIPLE HELIX/TRIPLE HELIX.tja
Normal file
File diff suppressed because it is too large
Load Diff
@@ -727,6 +727,10 @@ class AudioEngine:
|
||||
|
||||
self.running = False
|
||||
self.audio_device_ready = False
|
||||
self.sounds = {}
|
||||
self.music_streams = {}
|
||||
self.sound_queue = queue.Queue()
|
||||
self.music_queue = queue.Queue()
|
||||
print("Audio device closed")
|
||||
return
|
||||
|
||||
|
||||
@@ -176,7 +176,6 @@ class TJAParser:
|
||||
DIFFS = {0: "easy", 1: "normal", 2: "hard", 3: "oni", 4: "edit", 5: "tower", 6: "dan"}
|
||||
def __init__(self, path: Path, start_delay: int = 0, distance: int = 866):
|
||||
self.file_path: Path = path
|
||||
print(self.file_path)
|
||||
|
||||
lines = self.file_path.read_text(encoding='utf-8-sig').splitlines()
|
||||
self.data = [cleaned for line in lines
|
||||
|
||||
683
libs/utils.py
683
libs/utils.py
@@ -7,10 +7,15 @@ import zipfile
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
import pyray as ray
|
||||
import tomlkit
|
||||
from raylib import (
|
||||
SHADER_UNIFORM_FLOAT,
|
||||
SHADER_UNIFORM_VEC2,
|
||||
SHADER_UNIFORM_VEC4,
|
||||
)
|
||||
|
||||
#TJA Format creator is unknown. I did not create the format, but I did write the parser though.
|
||||
|
||||
@@ -231,600 +236,150 @@ class GlobalData:
|
||||
|
||||
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 __init__(self, text: str, font_size: int, color: ray.Color, outline_color: ray.Color, outline_thickness=5.0, vertical=False):
|
||||
self.font = self._load_font_for_text(text)
|
||||
if vertical:
|
||||
self.texture = self._create_text_vertical(text, font_size, color, ray.BLANK, self.font)
|
||||
else:
|
||||
self.texture = self._create_text_horizontal(text, font_size, color, ray.BLANK, self.font)
|
||||
outline_size = ray.ffi.new('float*', outline_thickness)
|
||||
if isinstance(outline_color, tuple):
|
||||
outline_color_alloc = ray.ffi.new("float[4]", [
|
||||
outline_color[0] / 255.0,
|
||||
outline_color[1] / 255.0,
|
||||
outline_color[2] / 255.0,
|
||||
outline_color[3] / 255.0
|
||||
])
|
||||
else:
|
||||
outline_color_alloc = ray.ffi.new("float[4]", [
|
||||
outline_color.r / 255.0,
|
||||
outline_color.g / 255.0,
|
||||
outline_color.b / 255.0,
|
||||
outline_color.a / 255.0
|
||||
])
|
||||
texture_size = ray.ffi.new("float[2]", [self.texture.width, self.texture.height])
|
||||
|
||||
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()
|
||||
self.shader = ray.load_shader('', 'shader/outline.fs')
|
||||
outline_size_loc = ray.get_shader_location(self.shader, "outlineSize")
|
||||
outline_color_loc = ray.get_shader_location(self.shader, "outlineColor")
|
||||
texture_size_loc = ray.get_shader_location(self.shader, "textureSize")
|
||||
self.alpha_loc = ray.get_shader_location(self.shader, "alpha")
|
||||
ray.set_shader_value(self.shader, outline_size_loc, outline_size, SHADER_UNIFORM_FLOAT)
|
||||
ray.set_shader_value(self.shader, outline_color_loc, outline_color_alloc, SHADER_UNIFORM_VEC4)
|
||||
ray.set_shader_value(self.shader, texture_size_loc, texture_size, SHADER_UNIFORM_VEC2)
|
||||
|
||||
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)
|
||||
return ray.load_font_ex(str(Path('Graphics/Modified-DFPKanteiryu-XB.ttf')), 40, 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 _create_text_vertical(self, text: str, font_size: int, color: ray.Color, bg_color: ray.Color, font: Optional[ray.Font]=None, padding: int=10):
|
||||
rotate_chars = {'-', '‐', '|', '/', '\\', 'ー', '~', '~', '(', ')', '(', ')',
|
||||
'「', '」', '[', ']', '[', ']', '【', '】', '…', '→', '→', ':', ':'}
|
||||
max_char_width = 0
|
||||
total_height = padding * 2 # Top and bottom padding
|
||||
|
||||
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)
|
||||
for char in text:
|
||||
if font:
|
||||
char_size = ray.measure_text_ex(font, char, font_size, 0)
|
||||
else:
|
||||
# Add character to current segment
|
||||
current_segment += self.text[i]
|
||||
i += 1
|
||||
char_width = ray.measure_text(char, font_size)
|
||||
char_size = ray.Vector2(char_width, font_size)
|
||||
|
||||
# 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)
|
||||
# If character should be rotated, swap width and height for measurements
|
||||
if char in rotate_chars:
|
||||
effective_width = char_size.y # Height becomes width when rotated 90°
|
||||
else:
|
||||
self._char_size_cache[char] = ray.measure_text_ex(self.font, char, self.font_size, 1.0)
|
||||
return self._char_size_cache[char]
|
||||
effective_width = char_size.x
|
||||
|
||||
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())
|
||||
max_char_width = max(max_char_width, effective_width)
|
||||
|
||||
# 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
|
||||
total_height += len(text) * font_size
|
||||
width = int(max_char_width + (padding * 2)) # Add left and right padding
|
||||
height = total_height
|
||||
image = ray.gen_image_color(width, height, bg_color)
|
||||
|
||||
# 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
|
||||
for i, char in enumerate(text):
|
||||
char_y = i * ray.measure_text_ex(self.font, char, font_size, 0).y
|
||||
char_y += padding
|
||||
|
||||
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)
|
||||
if font:
|
||||
char_size = ray.measure_text_ex(font, char, font_size, 0)
|
||||
char_image = ray.image_text_ex(font, char, font_size, 0, color)
|
||||
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)
|
||||
char_width = ray.measure_text(char, font_size)
|
||||
char_size = ray.Vector2(char_width, font_size)
|
||||
char_image = ray.image_text(char, font_size, color)
|
||||
|
||||
# Rotate character if it's in the rotate_chars set
|
||||
if char in rotate_chars:
|
||||
rotated_image = ray.gen_image_color(char_image.height, char_image.width, ray.BLANK)
|
||||
|
||||
# Manual 90-degree clockwise rotation
|
||||
for y in range(char_image.height):
|
||||
for x in range(char_image.width):
|
||||
src_color = ray.get_image_color(char_image, x, y)
|
||||
# 90° clockwise: new_x = old_y, new_y = width - 1 - old_x
|
||||
new_x = y
|
||||
new_y = char_image.width - 1 - x
|
||||
ray.image_draw_pixel(rotated_image, new_x, new_y, src_color)
|
||||
|
||||
ray.unload_image(char_image)
|
||||
char_image = rotated_image
|
||||
effective_width = char_size.y # Height becomes width when rotated
|
||||
else:
|
||||
# Parse text into segments
|
||||
segments = self._parse_text_segments()
|
||||
effective_width = char_size.x
|
||||
|
||||
char_heights = []
|
||||
char_widths = []
|
||||
# Center the character horizontally
|
||||
char_x = width // 2 - effective_width // 2
|
||||
|
||||
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'])
|
||||
ray.image_draw(image, char_image,
|
||||
ray.Rectangle(0, 0, char_image.width, char_image.height),
|
||||
ray.Rectangle(char_x, char_y, char_image.width, char_image.height),
|
||||
ray.WHITE)
|
||||
ray.unload_image(char_image)
|
||||
|
||||
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')
|
||||
texture = ray.load_texture_from_image(image)
|
||||
ray.unload_image(image)
|
||||
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)
|
||||
def _create_text_horizontal(self, text: str, font_size: int, color: ray.Color, bg_color: ray.Color, font: Optional[ray.Font]=None, padding: int=10):
|
||||
if font:
|
||||
text_size = ray.measure_text_ex(font, text, font_size, 0)
|
||||
total_width = text_size.x + (padding * 2)
|
||||
total_height = text_size.y + (padding * 2)
|
||||
else:
|
||||
self._draw_vertical_text(image, width)
|
||||
total_width = ray.measure_text(text, font_size) + (padding * 2)
|
||||
total_height = font_size + (padding * 2)
|
||||
image = ray.gen_image_color(int(total_width), int(total_height), bg_color)
|
||||
if font:
|
||||
text_image = ray.image_text_ex(font, text, font_size, 0, color)
|
||||
else:
|
||||
text_image = ray.image_text(text, font_size, color)
|
||||
text_x = padding
|
||||
text_y = padding
|
||||
ray.image_draw(image, text_image,
|
||||
ray.Rectangle(0, 0, text_image.width, text_image.height),
|
||||
ray.Rectangle(text_x, text_y, text_image.width, text_image.height),
|
||||
ray.WHITE)
|
||||
ray.unload_image(text_image)
|
||||
|
||||
ray.export_image(image, f'cache/image/{self.hash}.png')
|
||||
text_cache.add(self.hash)
|
||||
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):
|
||||
if isinstance(color, tuple):
|
||||
alpha_value = ray.ffi.new('float*', color[3] / 255.0)
|
||||
else:
|
||||
alpha_value = ray.ffi.new('float*', color.a / 255.0)
|
||||
ray.set_shader_value(self.shader, self.alpha_loc, alpha_value, SHADER_UNIFORM_FLOAT)
|
||||
ray.begin_shader_mode(self.shader)
|
||||
ray.draw_texture_pro(self.texture, src, dest, origin, rotation, color)
|
||||
ray.end_shader_mode()
|
||||
|
||||
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_shader(self.shader)
|
||||
ray.unload_texture(self.texture)
|
||||
|
||||
32
scenes/devtest.py
Normal file
32
scenes/devtest.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import pyray as ray
|
||||
|
||||
from libs.utils import OutlinedText, get_current_ms
|
||||
|
||||
|
||||
class DevScreen:
|
||||
def __init__(self, width: int, height: int):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.screen_init = False
|
||||
|
||||
self.time_now = get_current_ms()
|
||||
self.test = OutlinedText('Triple Helix', 40, ray.Color(255, 255, 255, 255), ray.Color(101, 0, 82, 255), outline_thickness=4, vertical=True)
|
||||
|
||||
def on_screen_start(self):
|
||||
if not self.screen_init:
|
||||
self.screen_init = True
|
||||
|
||||
def on_screen_end(self, next_screen: str):
|
||||
self.screen_init = False
|
||||
return next_screen
|
||||
|
||||
def update(self):
|
||||
self.on_screen_start()
|
||||
if ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER):
|
||||
return self.on_screen_end('TITLE')
|
||||
|
||||
def draw(self):
|
||||
ray.draw_rectangle(0, 0, self.width, self.height, ray.WHITE)
|
||||
src = ray.Rectangle(0, 0, self.test.texture.width, self.test.texture.height)
|
||||
dest = ray.Rectangle(self.width//2 - self.test.texture.width//2, self.height//2 - self.test.texture.height//2, self.test.texture.width, self.test.texture.height)
|
||||
self.test.draw(src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
|
||||
@@ -279,12 +279,12 @@ class EntryScreen:
|
||||
|
||||
box_title = self.box_titles[i][1]
|
||||
src = ray.Rectangle(0, 0, box_title.texture.width, box_title.texture.height)
|
||||
dest = ray.Rectangle(final_x + 12, y + 20, box_title.texture.width, box_title.texture.height)
|
||||
dest = ray.Rectangle(final_x + 25, y + 20, box_title.texture.width, box_title.texture.height)
|
||||
box_title.draw(src, dest, ray.Vector2(0, 0), 0, color)
|
||||
else:
|
||||
box_title = self.box_titles[i][0]
|
||||
src = ray.Rectangle(0, 0, box_title.texture.width, box_title.texture.height)
|
||||
dest = ray.Rectangle(final_x + 9, y + 20, box_title.texture.width, box_title.texture.height)
|
||||
dest = ray.Rectangle(final_x + 20, y + 20, box_title.texture.width, box_title.texture.height)
|
||||
box_title.draw(src, dest, ray.Vector2(0, 0), 0, color)
|
||||
|
||||
def draw(self):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pyray as ray
|
||||
import sounddevice as sd
|
||||
|
||||
from libs.audio import audio
|
||||
from libs.utils import (
|
||||
global_data,
|
||||
is_l_don_pressed,
|
||||
@@ -34,6 +35,9 @@ class SettingsScreen:
|
||||
self.screen_init = False
|
||||
save_config(self.config)
|
||||
global_data.config = self.config
|
||||
audio.close_audio_device()
|
||||
audio.type = global_data.config["audio"]["device_type"]
|
||||
audio.init_audio_device()
|
||||
return "ENTRY"
|
||||
|
||||
def get_current_settings(self):
|
||||
|
||||
@@ -400,7 +400,7 @@ class SongBox:
|
||||
self.tja_count = tja_count
|
||||
self.tja_count_text = None
|
||||
if self.tja_count is not None and self.tja_count != 0:
|
||||
self.tja_count_text = OutlinedText(str(self.tja_count), 35, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, horizontal_spacing=1.2)
|
||||
self.tja_count_text = OutlinedText(str(self.tja_count), 35, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5)#, horizontal_spacing=1.2)
|
||||
self.tja = tja
|
||||
self.hash = dict()
|
||||
self.update(False)
|
||||
@@ -1007,6 +1007,8 @@ class SongFile(FileSystemItem):
|
||||
def __init__(self, path: Path, name: str, texture_index: int, tja=None, name_texture_index: Optional[int]=None):
|
||||
super().__init__(path, name)
|
||||
self.is_recent = (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime)) <= timedelta(days=7)
|
||||
if self.is_recent:
|
||||
print(name, (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime)))
|
||||
self.tja = tja or TJAParser(path)
|
||||
if self.is_recent:
|
||||
self.tja.ex_data.new = True
|
||||
|
||||
40
shader/outline.fs
Normal file
40
shader/outline.fs
Normal file
@@ -0,0 +1,40 @@
|
||||
#version 330
|
||||
in vec2 fragTexCoord;
|
||||
in vec4 fragColor;
|
||||
uniform sampler2D texture0;
|
||||
uniform vec4 colDiffuse;
|
||||
uniform vec2 textureSize;
|
||||
uniform float outlineSize;
|
||||
uniform vec4 outlineColor;
|
||||
uniform float alpha;
|
||||
uniform float smoothness = 1.0; // Add this uniform for control (0.0-1.0)
|
||||
out vec4 finalColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 texel = texture(texture0, fragTexCoord);
|
||||
vec2 texelScale = vec2(outlineSize/textureSize.x, outlineSize/textureSize.y);
|
||||
|
||||
// Calculate outline
|
||||
float outline = 0.0;
|
||||
int ringSamples = 64;
|
||||
int rings = 4;
|
||||
for(int ring = 1; ring <= rings; ring++) {
|
||||
float ringRadius = float(ring) / float(rings);
|
||||
for(int i = 0; i < ringSamples; i++) {
|
||||
float angle = 2.0 * 3.14159 * float(i) / float(ringSamples);
|
||||
vec2 offset = vec2(cos(angle), sin(angle)) * texelScale * ringRadius;
|
||||
outline += texture(texture0, fragTexCoord + offset).a / float(rings);
|
||||
}
|
||||
}
|
||||
outline = min(outline, 1.0);
|
||||
outline = smoothstep(0.1, 0.6, outline);
|
||||
|
||||
float edgeStart = 0.5 - smoothness * 0.3;
|
||||
float edgeEnd = 0.5 + smoothness * 0.3;
|
||||
float textAlpha = smoothstep(edgeStart, edgeEnd, texel.a);
|
||||
vec3 color = mix(outlineColor.rgb, texel.rgb, textAlpha);
|
||||
float combinedAlpha = mix(outline * outlineColor.a, texel.a, textAlpha);
|
||||
|
||||
finalColor = vec4(color, combinedAlpha * alpha);
|
||||
}
|
||||
Reference in New Issue
Block a user