Add renderer.py
This commit is contained in:
parent
0f0711ef2b
commit
3f60573b25
136
OpenFontFormat/renderer.py
Normal file
136
OpenFontFormat/renderer.py
Normal 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
96
OpenFontFormat/slot.py
Normal 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)
|
||||
|
Loading…
Reference in New Issue
Block a user