365 lines
9.6 KiB
Python
365 lines
9.6 KiB
Python
import os
|
|
import numpy as np
|
|
import cli_ui
|
|
import unicodedata
|
|
import json
|
|
import base64
|
|
import tkinter
|
|
from exporter import export
|
|
|
|
##### CONFIG #####
|
|
|
|
WINDOW_SIZE = (
|
|
(1280,720),
|
|
(1920,1019)
|
|
)[0]
|
|
|
|
GRID_COLOR = "#808080"
|
|
GRID_GUIDE_COLOR = (128, 128, 255)
|
|
GRID_SIZE = 32
|
|
cross_color = (255, 60, 25)
|
|
pixel_color = (255,) * 3
|
|
|
|
##################
|
|
|
|
project_data={}
|
|
project_chars = {}
|
|
project_char_res = None
|
|
current_project=""
|
|
current_char=33
|
|
canvas_width=0
|
|
canvas_height=0
|
|
|
|
def check_char_exist(char_num):
|
|
global project_chars
|
|
return chr(char_num) in project_chars
|
|
|
|
def draw_editor_canvas():
|
|
global canvas_editor
|
|
global project_char_res
|
|
global canvas_width
|
|
global canvas_height
|
|
|
|
canvas_editor.delete("all")
|
|
|
|
# draw grid
|
|
for x in range(project_char_res[0] + 1):
|
|
x = x * GRID_SIZE
|
|
canvas_editor.create_line((x,0),(x,canvas_height),width=1,fill=GRID_COLOR)
|
|
for y in range(project_char_res[1] + 1):
|
|
y = y * GRID_SIZE
|
|
canvas_editor.create_line((0,y),(canvas_width,y),width=1,fill=GRID_COLOR)
|
|
|
|
if check_char_exist(current_char):
|
|
pass
|
|
else:
|
|
canvas_editor.create_line((0,0),(canvas_width,canvas_height),width=3,fill="red")
|
|
canvas_editor.create_line((canvas_width,0),(0,canvas_height),width=3,fill="red")
|
|
|
|
|
|
def menu_file_open_project_click():
|
|
global project_data
|
|
global project_chars
|
|
global project_char_res
|
|
global last_project
|
|
global canvas_editor
|
|
global canvas_width
|
|
global canvas_height
|
|
|
|
file_path = tkinter.filedialog.askopenfilename(
|
|
title="Open font project",
|
|
filetypes=[("Font project (json)", "*.fontproj")]
|
|
)
|
|
if file_path == "":
|
|
return
|
|
|
|
# save last path to cache
|
|
with open("last_path_cache.txt", "w") as f:
|
|
f.write(file_path)
|
|
current_project=file_path
|
|
|
|
with open(file_path, "r") as f:
|
|
project_data = json.load(f)
|
|
|
|
project_chars = project_data["chars"]
|
|
project_char_res = (project_data["char_width"], project_data["char_height"])
|
|
|
|
canvas_width=project_char_res[0]*GRID_SIZE
|
|
canvas_height=project_char_res[1]*GRID_SIZE
|
|
canvas_editor.config(width=canvas_width,height=canvas_height)
|
|
|
|
draw_editor_canvas()
|
|
|
|
|
|
def menu_file_save_project_click():
|
|
pass
|
|
|
|
def menu_file_save_project_as_click():
|
|
pass
|
|
|
|
def button_prev_glyph_click():
|
|
global current_char
|
|
current_char-=1
|
|
keep_char_num_in_bounds()
|
|
draw_editor_canvas()
|
|
|
|
|
|
def button_next_glyph_click():
|
|
global current_char
|
|
current_char+=1
|
|
keep_char_num_in_bounds()
|
|
draw_editor_canvas()
|
|
|
|
|
|
def keep_char_num_in_bounds():
|
|
global current_char
|
|
if current_char<0:
|
|
current_char=0
|
|
elif current_char>99999:
|
|
current_char=99999
|
|
|
|
|
|
def number_only_validate(val):
|
|
return str.isdigit(val) or val==""
|
|
|
|
|
|
window=tkinter.Tk()
|
|
window.title("fonteditor")
|
|
window.geometry(f"{WINDOW_SIZE[0]}x{WINDOW_SIZE[1]}")
|
|
|
|
menubar=tkinter.Menu(window)
|
|
menu_file=tkinter.Menu(menubar,tearoff=False)
|
|
menu_file.add_command(label="Open project",command=menu_file_open_project_click)
|
|
menu_file.add_command(label="Save project",command=menu_file_save_project_click)
|
|
menu_file.add_command(label="Save project as",command=menu_file_save_project_as_click)
|
|
menubar.add_cascade(label="File",menu=menu_file)
|
|
|
|
canvas_editor=tkinter.Canvas(window,bg="black")
|
|
canvas_editor.pack(side="left")
|
|
|
|
frame_controls=tkinter.Frame(window)
|
|
frame_controls.pack()
|
|
|
|
canvas_preview=tkinter.Canvas(frame_controls,width=100,height=200,bg="black")
|
|
canvas_preview.pack(side="top")
|
|
|
|
label_glyph_name=tkinter.Label(frame_controls,text="Placeholder")
|
|
label_glyph_name.pack(side="top")
|
|
|
|
frame_nav=tkinter.Frame(frame_controls)
|
|
frame_nav.pack(side="top",pady=10)
|
|
|
|
button_prev_glyph=tkinter.Button(frame_nav,width=10,text="Previous",command=button_prev_glyph_click)
|
|
button_prev_glyph.pack(side="left")
|
|
|
|
button_next_glyph=tkinter.Button(frame_nav,width=10,text="Next",command=button_next_glyph_click)
|
|
button_next_glyph.pack(side="left")
|
|
|
|
frame_glyph_id=tkinter.Frame(frame_controls)
|
|
frame_glyph_id.pack(side="top",pady=10)
|
|
|
|
entry_glyph_id=tkinter.Entry(frame_glyph_id,validate="all",validatecommand=((window.register(number_only_validate)),"%P"))
|
|
entry_glyph_id.pack(side="left")
|
|
|
|
button_glyph_search=tkinter.Button(frame_glyph_id,width=10,text="Search")
|
|
button_glyph_search.pack(side="left")
|
|
|
|
window.config(menu=menubar)
|
|
window.mainloop()
|
|
|
|
"""proj_data, proj_path = cli_ui.cli_main()
|
|
pixel_size = (window_size[1]-1) / char_res[1]
|
|
canva_width = pixel_size * char_res[0]"""
|
|
|
|
def cursor_pos_to_pixel(pos):
|
|
pixel_pos = (int(pos[0] // pixel_size), int(pos[1] // pixel_size))
|
|
|
|
if pixel_pos[0] < 0 or pixel_pos[1] < 0 or pixel_pos[0] >= char_res[0] or pixel_pos[1] >= char_res[1]:
|
|
return None
|
|
|
|
return pixel_pos
|
|
|
|
|
|
# main loop
|
|
while True:
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
pygame.quit()
|
|
quit()
|
|
|
|
elif event.type == pygame.MOUSEWHEEL:
|
|
char_num += event.y * -1
|
|
|
|
does_char_exist = check_char_exist(char_num)
|
|
|
|
|
|
elif event.type == pygame.KEYDOWN:
|
|
|
|
# delete char
|
|
if event.key == pygame.K_DELETE:
|
|
if does_char_exist:
|
|
del chars[chr(char_num)]
|
|
does_char_exist = False
|
|
force_deleted = True
|
|
|
|
# toggle grid
|
|
if event.key == pygame.K_g:
|
|
is_grid = not is_grid
|
|
|
|
# export font
|
|
elif event.key == pygame.K_e:
|
|
export(char_res, chars)
|
|
|
|
# print key tips
|
|
elif event.key == pygame.K_p:
|
|
cli_ui.key_tips()
|
|
|
|
|
|
# initialize current stroke
|
|
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
|
pixel_pos = cursor_pos_to_pixel(pygame.mouse.get_pos())
|
|
# if char is drawable at current position
|
|
if pixel_pos is not None:
|
|
# create char dictionary entry if it does not exist
|
|
if not does_char_exist:
|
|
chars[chr(char_num)] = np.zeros(char_res, dtype=bool)
|
|
does_char_exist = True
|
|
|
|
paint_color = not chars[chr(char_num)][pixel_pos]
|
|
|
|
# save at the end of the stroke
|
|
elif (event.type == pygame.MOUSEBUTTONUP and event.button == 1) or force_deleted:
|
|
force_deleted = False
|
|
|
|
## packbit all chars
|
|
|
|
# calculate the number of bits needed to pad char array to a multiple of 8
|
|
remainder = char_res[0] * char_res[1] % 8
|
|
padding = (8 - remainder) % 8
|
|
|
|
|
|
packed_chars = {}
|
|
for key, value in chars.items():
|
|
# reshape into 1D array
|
|
new_data = value.reshape(-1)
|
|
# pad
|
|
new_data = np.pad(new_data, (0, padding), mode="constant")
|
|
# reshape the padded array into a 2D array with 8 columns
|
|
new_data = new_data.reshape(-1, 8)
|
|
# packbits
|
|
new_data = np.packbits(new_data, axis=1).tobytes()
|
|
# convert to base64
|
|
new_data = base64.b64encode(new_data).decode("utf-8")
|
|
|
|
packed_chars[key] = new_data
|
|
|
|
proj_data["chars"] = packed_chars
|
|
|
|
with open(proj_path, "w", encoding="utf-8") as f:
|
|
json.dump(proj_data, f, indent=4, ensure_ascii=False)
|
|
|
|
print("Autosaved project.")
|
|
|
|
|
|
|
|
|
|
# perform drawing
|
|
if pygame.mouse.get_pressed()[0] and does_char_exist:
|
|
pixel_pos = cursor_pos_to_pixel(pygame.mouse.get_pos())
|
|
|
|
if pixel_pos is not None:
|
|
chars[chr(char_num)][pixel_pos] = paint_color
|
|
|
|
# if all pixels are off, delete char
|
|
if not np.any(chars[chr(char_num)]):
|
|
del chars[chr(char_num)]
|
|
does_char_exist = False
|
|
|
|
|
|
window.fill((0, 0, 0))
|
|
|
|
|
|
if does_char_exist:
|
|
current_char_array = chars[chr(char_num)]
|
|
# draw pixels from current_char_array
|
|
for y in range(char_res[1]):
|
|
for x in range(char_res[0]):
|
|
if current_char_array[x, y]:
|
|
pygame.draw.rect(
|
|
window,
|
|
pixel_color,
|
|
(
|
|
x * pixel_size,
|
|
y * pixel_size,
|
|
pixel_size,
|
|
pixel_size
|
|
)
|
|
)
|
|
|
|
# draw char num
|
|
font.render_to(
|
|
window,
|
|
(canva_width + 30, 30),
|
|
f"dec: {char_num}",
|
|
(255, 255, 255),
|
|
size=64,
|
|
)
|
|
|
|
|
|
# draw char
|
|
font.render_to(
|
|
window,
|
|
(canva_width + 30, 100),
|
|
chr(char_num),
|
|
(255, 255, 255),
|
|
size=150,
|
|
)
|
|
|
|
# draw alternate font char
|
|
alt_font.render_to(
|
|
window,
|
|
(canva_width + 200, 100),
|
|
chr(char_num),
|
|
(255, 255, 255),
|
|
size=256,
|
|
)
|
|
|
|
|
|
# draw char name
|
|
font.render_to(
|
|
window,
|
|
(canva_width + 30, 280),
|
|
unicodedata.name(chr(char_num), "unknown"),
|
|
(255, 255, 255),
|
|
size=16,
|
|
)
|
|
|
|
|
|
# draw a red cross if curren char does not exist
|
|
if not does_char_exist:
|
|
pygame.draw.line(
|
|
window,
|
|
cross_color,
|
|
(0, 0),
|
|
(canva_width, window_size[1]),
|
|
7
|
|
)
|
|
pygame.draw.line(
|
|
window,
|
|
cross_color,
|
|
(0, window_size[1]),
|
|
(canva_width, 0),
|
|
7
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
font.render_to(window, (10, 10), f"FPS: {clock.get_fps():.2f}", (255, 255, 255), size=16)
|
|
|
|
pygame.display.update()
|
|
clock.tick(fps_limit)
|
|
#d_time = clock.tick(fps_limit) / 1000
|