Add renderer.py

This commit is contained in:
germax26 2024-09-15 16:23:16 +10:00
parent 0f0711ef2b
commit 3f60573b25
Signed by: germax26
SSH Key Fingerprint: SHA256:N3w+8798IMWBt7SYH8G1C0iJlIa2HIIcRCXwILT5FvM
2 changed files with 232 additions and 0 deletions

136
OpenFontFormat/renderer.py Normal file
View File

@ -0,0 +1,136 @@
import sys
from typing import Any, Generator, List, Optional, Tuple
from pyray import GRAY, GREEN, RED, WHITE, YELLOW, Color as Colour, ConfigFlags, KeyboardKey, MouseButton, Rectangle, TextureFilter, Vector2, begin_drawing, check_collision_point_rec, clear_background, close_window, draw_circle, draw_circle_3d, draw_circle_v, draw_line, draw_line_ex, draw_rectangle_lines_ex, draw_rectangle_rec, draw_spline_bezier_quadratic, draw_text, draw_texture_pro, end_drawing, gen_texture_mipmaps, get_char_pressed, get_color as get_colour, get_mouse_position, get_mouse_wheel_move, get_screen_height, get_screen_width, get_time, init_window, is_mouse_button_pressed, is_mouse_button_released, load_image, load_texture_from_image, set_config_flags, set_exit_key, set_target_fps, set_texture_filter, vector2_zero, window_should_close # type: ignore
from OpenFont import CompoundGlyph, Glyph, SimpleGlyph, SimpleGlyphFlag, TrueTypeOutlines, get_full_name, get_glyph_index, glyph_count, open_font_file
from slot import Slot
import builtins
_program, file_path = sys.argv
file = open_font_file(file_path)
font_x_min, font_y_min, font_x_max, font_y_max = file.font_header.xMin, file.font_header.yMin, file.font_header.xMax, file.font_header.yMax
SCALE = 0.5
font_x_range = font_x_max-font_x_min
font_y_range = font_y_max-font_y_min
SCREEN_WIDTH = int(font_x_range*SCALE)
SCREEN_HEIGHT = int(font_y_range*SCALE)
COLOUR_BACKGROUND = get_colour(0x181818FF)
def middle_point(point1: Tuple[int, int], point2: Tuple[int, int]) -> Tuple[int, int]:
(x0, y0), (x1, y1) = point1, point2
return (x0+x1)//2, (y0+y1)//2
def triple_iter(coordinates: List[Tuple[int, int]], flags: List[SimpleGlyphFlag]) -> Generator[Tuple[Tuple[int,int], Tuple[int,int], Tuple[int,int]], None, None]:
assert flags[0].on_curve_point()
coordinates_and_flags = list(zip(coordinates, flags))
all_points: List[Tuple[int, int]] = []
for (point1, flag1), (point2, flag2) in zip(coordinates_and_flags,coordinates_and_flags[1:]+[coordinates_and_flags[0]]):
all_points.append(point1)
if flag1.on_curve_point() == flag2.on_curve_point(): all_points.append(middle_point(point1, point2))
all_points.append(point2)
assert len(all_points) % 2 != 0
for i in range(1,len(all_points),2):
yield all_points[i-1], all_points[i], all_points[i+1]
def draw_bounding_box(slot: Slot, glyph: Glyph, colour: Colour=RED):
bounding_rect = Rectangle(glyph.xMin, glyph.yMin, glyph.xMax-glyph.xMin, glyph.yMax-glyph.yMin)
draw_rectangle_lines_ex(slot.r(bounding_rect), 1, colour)
def render_simple_glyph(slot: Slot, glyph: SimpleGlyph):
"""
Renders a simple glyph into a slot in coordinate space
"""
draw_bounding_box(slot, glyph)
for prev_end_point, end_point in zip([-1]+glyph.endPtsOfContours, glyph.endPtsOfContours):
for point1, point2, point3 in triple_iter(glyph.coordinates[prev_end_point+1:end_point+1], glyph.flags[prev_end_point+1:end_point+1]):
draw_spline_bezier_quadratic([slot.vxy(*point1), slot.vxy(*point2), slot.vxy(*point3)], 3, 2, WHITE)
def render_box_and_get_slot(slot: Slot) -> Tuple[Slot, Slot]:
"""
Renders the outer box and axes lines into a slot in UV space and returns the outer box slot and the slot in coordinate space
"""
with slot.padding(0.025, 0.025) as slot:
draw_rectangle_lines_ex(slot.r(), 1, GRAY)
outer_slot = slot
with slot.padding(0.05, 0.05).with_ratio(font_x_range/font_y_range).remap(font_x_min, font_y_max, font_x_max, font_y_min) as slot:
draw_line_ex(slot.vxy(font_x_min, 0), slot.vxy(font_x_max, 0), 1, WHITE)
draw_line_ex(slot.vxy(0, font_y_min), slot.vxy(0, font_y_max), 1, WHITE)
return outer_slot, slot
def render_glyph(slot: Slot, glyphID: int):
# TODO: Keep the current display mode as a 'wireframe' mode and allow user to switch between normal and wireframe modes
"""
Renders a glyph into a slot in coordinates space
"""
assert isinstance(file.outlines, TrueTypeOutlines)
glyph = file.outlines.glyph_data.glyphs[glyphID]
match glyph:
case SimpleGlyph(): render_simple_glyph(slot, glyph)
case CompoundGlyph(components=components):
draw_bounding_box(slot, glyph)
for component in components:
# TODO: Scaling
# arg1 and arg2 will also need to be scaled, depending on the values of scaled_component_offset and unscaled_component_offset
assert component.scaling is None, f"Unimplemented: {component.scaling}"
assert component.flag.args_are_xy_values(), "Unimplemented"
# assert not component.flag.round_xy_to_grid(), ("Unimplemented", component.argument1, component.argument2)
render_glyph(slot.translate(component.argument1, component.argument2), component.glyphIndex)
case None: pass # TODO: Render bounding box of empty glyphs
set_config_flags(ConfigFlags.FLAG_WINDOW_ALWAYS_RUN | ConfigFlags.FLAG_WINDOW_RESIZABLE)
init_window(SCREEN_WIDTH, SCREEN_HEIGHT, get_full_name(file))
set_target_fps(60)
set_exit_key(KeyboardKey.KEY_NULL)
COLS = 8
ROWS = 8
scroll_y: float = 0
MAX_SCROLL_IN_SCREEN_HEIGHTS = (glyph_count(file)+COLS-1)//COLS/ROWS-1
SCROLL_SPEED = 10
def rectangle_is_visible(rect: Rectangle) -> bool:
return -rect.width <= rect.x <= get_screen_width() and -rect.height <= rect.y <= get_screen_height()
def clamp_scroll(scroll_y: float) -> float:
return max(min(scroll_y,MAX_SCROLL_IN_SCREEN_HEIGHTS),0)
display_in_detail: Optional[int] = None
while not window_should_close():
scroll_y -= get_mouse_wheel_move()*SCROLL_SPEED/get_screen_height()
scroll_y = clamp_scroll(scroll_y)
begin_drawing()
clear_background(COLOUR_BACKGROUND)
with Slot.get_root_slot() as root:
curr_display_detail = display_in_detail
with root.translate(0, -scroll_y) as slot:
for i, slot in enumerate(slot.extending_grid(ROWS, COLS, -1, total_slots=glyph_count(file))):
if not rectangle_is_visible(slot.r()): continue
outer_slot, glyph_slot = render_box_and_get_slot(slot)
render_glyph(glyph_slot, i)
if display_in_detail is None and is_mouse_button_released(MouseButton.MOUSE_BUTTON_LEFT):
if check_collision_point_rec(get_mouse_position(), outer_slot.r()): display_in_detail = i
if curr_display_detail is not None:
with root.padding(0.2, 0.2) as slot:
draw_rectangle_rec(slot.r(), COLOUR_BACKGROUND)
outer_slot, glyph_slot = render_box_and_get_slot(slot)
render_glyph(glyph_slot, curr_display_detail)
if is_mouse_button_released(MouseButton.MOUSE_BUTTON_LEFT): display_in_detail = None
# TODO: Use glfw_set_char_callback for this instead
char = get_char_pressed()
while char > 0:
display_in_detail = get_glyph_index(file, chr(char))
char = get_char_pressed()
# draw_text(f"{scroll_y}", 50, 50, 50, GREEN) # debug for scrolling
end_drawing()
close_window()

96
OpenFontFormat/slot.py Normal file
View File

@ -0,0 +1,96 @@
from typing import Callable
from pyray import Rectangle, Vector2, get_screen_height, get_screen_width # type: ignore
Map = Callable[[float], float]
class Slot:
def __init__(self, x_map: Map, y_map: Map):
self.x = x_map
self.y = y_map
def v(self, v: Vector2) -> Vector2:
return Vector2(self.x(v.x), self.y(v.y))
def __enter__(self): return self
def __exit__(self, exc_type, exc_value, trace): pass
def vxy(self, x: float, y: float) -> Vector2:
return self.v(Vector2(x, y))
def r(self, r: Rectangle=Rectangle(0,0,1,1)) -> Rectangle:
x0 = self.x(r.x)
y0 = self.y(r.y)
x1 = self.x(r.x+r.width)
y1 = self.y(r.y+r.height)
x_min = min(x0, x1)
y_min = min(y0, y1)
x_max = max(x0, x1)
y_max = max(y0, y1)
return Rectangle(x_min, y_min, x_max-x_min, y_max-y_min)
@staticmethod
def get_root_slot(*, in_uv:bool=True) -> 'Slot':
"""
Returns the root slot for the window
"""
screen_width = get_screen_width()
screen_height = get_screen_height()
if in_uv:
return Slot(lambda x: x*screen_width, lambda y: y*screen_height)
else:
return Slot(lambda x: x, lambda y: y)
def between(self, x0: float, y0: float, x1: float, y1: float) -> 'Slot':
return Slot(lambda x: self.x(x*(x1-x0)+x0), lambda y: self.y(y*(y1-y0)+y0))
def remap(self, x0: float, y0: float, x1: float, y1: float) -> 'Slot':
"""
Returns the slot with the same region on the window but using a different coordinate space
"""
return Slot(lambda x: self.x((x-x0)/(x1-x0)), lambda y: self.y((y-y0)/(y1-y0)))
def grid(self, rows: int, cols: int, *, total_slots:int=-1) -> 'list[Slot]':
return self.extending_grid(rows, cols, rows, total_slots=total_slots)
def extending_grid(self, rows: int, cols: int, total_rows: int, *, total_slots:int=-1) -> 'list[Slot]':
slots: list[Slot] = []
if total_rows == -1:
assert total_slots != -1
total_rows = (total_slots+cols-1)//cols
for j in range(total_rows):
for i in range(cols):
slots.append(self.between(i/cols, j/rows, (i+1)/cols, (j+1)/rows))
if total_slots != -1: return slots[:total_slots]
return slots
def translate(self, x0: float, y0: float) -> 'Slot':
return self.between(x0, y0, x0+1, y0+1)
def square(self) -> 'Slot':
"""
Returns the square slot that takes up as much space as possible within parent slot
"""
return self.with_ratio(1)
def with_ratio(self, width_to_height_ratio: float) -> 'Slot':
width = self.x(1) - self.x(0)
height = self.y(1) - self.y(0)
if width == 0 or height == 0: return Slot(lambda x: self.x(0) + width/2, lambda y: self.y(0) + height/2)
if width/height > width_to_height_ratio:
ratio = height/width*width_to_height_ratio
left = (1-ratio)/2
return self.between(left, 0, 1-left, 1)
else:
ratio = width/height/width_to_height_ratio
up = (1-ratio)/2
return self.between(0, up, 1, 1-up)
def padding(self, horz_padding: float, vert_padding: float, *, width:float=1, height:float=1) -> 'Slot':
return self.between(horz_padding, vert_padding, width-horz_padding, height-horz_padding)