408 lines
17 KiB
Python
408 lines
17 KiB
Python
import math, time
|
|
import numpy as np
|
|
|
|
from ui_element import UIElement, UIArt, UIRenderable
|
|
from renderable_line import LineRenderable, SwatchSelectionBoxRenderable, UIRenderableX
|
|
|
|
# min width for charset; if charset is tiny adjust to this
|
|
MIN_CHARSET_WIDTH = 16
|
|
|
|
class UISwatch(UIElement):
|
|
|
|
def __init__(self, ui, popup):
|
|
self.ui = ui
|
|
self.popup = popup
|
|
self.reset()
|
|
|
|
def reset(self):
|
|
self.tile_width, self.tile_height = self.get_size()
|
|
art = self.ui.active_art
|
|
# generate a unique name for debug purposes
|
|
art_name = '%s_%s' % (int(time.time()), self.__class__.__name__)
|
|
self.art = UIArt(art_name, self.ui.app, art.charset, art.palette, self.tile_width, self.tile_height)
|
|
# tear down existing renderables if any
|
|
if not self.renderables:
|
|
self.renderables = []
|
|
else:
|
|
for r in self.renderables:
|
|
r.destroy()
|
|
self.renderables = []
|
|
self.renderable = UIRenderable(self.ui.app, self.art)
|
|
self.renderable.ui = self.ui
|
|
self.renderable.grain_strength = 0
|
|
self.renderables.append(self.renderable)
|
|
self.reset_art()
|
|
|
|
def reset_art(self):
|
|
pass
|
|
|
|
def get_size(self):
|
|
return 1, 1
|
|
|
|
def set_cursor_loc_from_mouse(self, cursor, mouse_x, mouse_y):
|
|
# get location within char map
|
|
w, h = self.art.quad_width, self.art.quad_height
|
|
tile_x = (mouse_x - self.x) / w
|
|
tile_y = (mouse_y - self.y) / h
|
|
self.set_cursor_loc(cursor, tile_x, tile_y)
|
|
|
|
def set_cursor_loc(self, cursor, tile_x, tile_y):
|
|
"""
|
|
common, generalized code for both character and palette swatches:
|
|
set cursor's screen location, tile location, and quad size.
|
|
"""
|
|
w, h = self.art.quad_width, self.art.quad_height
|
|
# snap to tile (tile_x/y already set in move_cursor)
|
|
tile_x = int(tile_x)
|
|
tile_y = int(tile_y)
|
|
# back to screen coords
|
|
x = tile_x * w + self.x
|
|
y = tile_y * h + self.y
|
|
tile_index = (abs(tile_y) * self.art.width) + tile_x
|
|
# if a valid character isn't hovered, bail
|
|
if not self.is_selection_index_valid(tile_index):
|
|
self.set_cursor_selection_index(-1)
|
|
return
|
|
# cool, set cursor location & size
|
|
self.set_cursor_selection_index(tile_index)
|
|
cursor.quad_size_ref = self.art
|
|
cursor.tile_x, cursor.tile_y = tile_x, tile_y
|
|
cursor.x, cursor.y = x, y
|
|
|
|
def is_selection_index_valid(self, index):
|
|
"returns True if given index is valid for choices this swatch offers"
|
|
return False
|
|
|
|
def set_cursor_selection_index(self, index):
|
|
"another set_cursor_loc support method, overriden by subclasses"
|
|
self.popup.blah = index
|
|
|
|
def render(self):
|
|
self.renderable.render()
|
|
|
|
|
|
class CharacterSetSwatch(UISwatch):
|
|
|
|
# scale the character set will be drawn at
|
|
char_scale = 2
|
|
min_scale = 1
|
|
max_scale = 5
|
|
scale_increment = 0.25
|
|
|
|
def increase_scale(self):
|
|
if self.char_scale <= self.max_scale - self.scale_increment:
|
|
self.char_scale += self.scale_increment
|
|
|
|
def decrease_scale(self):
|
|
if self.char_scale >= self.min_scale + self.scale_increment:
|
|
self.char_scale -= self.scale_increment
|
|
|
|
def reset(self):
|
|
UISwatch.reset(self)
|
|
self.selection_box = SwatchSelectionBoxRenderable(self.ui.app, self.art)
|
|
self.grid = CharacterGridRenderable(self.ui.app, self.art)
|
|
self.create_shade()
|
|
self.renderables = [self.renderable, self.selection_box, self.grid,
|
|
self.shade]
|
|
|
|
def create_shade(self):
|
|
# shaded box neath chars in case selected colors make em hard to see
|
|
self.shade_art = UIArt('charset_shade', self.ui.app,
|
|
self.ui.active_art.charset, self.ui.palette,
|
|
self.tile_width, self.tile_height)
|
|
self.shade_art.clear_frame_layer(0, 0, self.ui.colors.black)
|
|
self.shade = UIRenderable(self.ui.app, self.shade_art)
|
|
self.shade.ui = self.ui
|
|
self.shade.alpha = 0.2
|
|
|
|
def get_size(self):
|
|
art = self.ui.active_art
|
|
return art.charset.map_width, art.charset.map_height
|
|
|
|
def reset_art(self):
|
|
# MAYBE-TODO: using screen resolution, try to set quad size to an even
|
|
# multiple of screen so the sampling doesn't get chunky
|
|
aspect = self.ui.app.window_width / self.ui.app.window_height
|
|
charset = self.art.charset
|
|
self.art.quad_width = UIArt.quad_width * self.char_scale
|
|
self.art.quad_height = self.art.quad_width * (charset.char_height / charset.char_width) * aspect
|
|
# only need to populate characters on reset_art, but update
|
|
# colors every update()
|
|
self.art.clear_frame_layer(0, 0, 0)
|
|
i = 0
|
|
for y in range(charset.map_height):
|
|
for x in range(charset.map_width):
|
|
self.art.set_char_index_at(0, 0, x, y, i)
|
|
i += 1
|
|
self.art.geo_changed = True
|
|
|
|
def reset_loc(self):
|
|
self.x = self.popup.x + self.popup.swatch_margin
|
|
self.y = self.popup.y
|
|
self.y -= self.popup.art.quad_height * (self.popup.tab_height + 4)
|
|
self.renderable.x, self.renderable.y = self.x, self.y
|
|
self.grid.x, self.grid.y = self.x, self.y
|
|
self.grid.y -= self.art.quad_height
|
|
self.shade.x, self.shade.y = self.x, self.y
|
|
|
|
def set_xform(self, new_xform):
|
|
for y in range(self.art.height):
|
|
for x in range(self.art.width):
|
|
self.art.set_char_transform_at(0, 0, x, y, new_xform)
|
|
|
|
def is_selection_index_valid(self, index):
|
|
return index < self.art.charset.last_index
|
|
|
|
def set_cursor_selection_index(self, index):
|
|
self.popup.cursor_char = index
|
|
self.popup.cursor_color = -1
|
|
|
|
def move_cursor(self, cursor, dx, dy):
|
|
"moves cursor by specified amount in selection grid"
|
|
# determine new cursor tile X/Y
|
|
tile_x = cursor.tile_x + dx
|
|
tile_y = cursor.tile_y + dy
|
|
tile_index = (abs(tile_y) * self.art.width) + tile_x
|
|
if tile_x < 0 or tile_x >= self.art.width:
|
|
return
|
|
elif tile_y > 0:
|
|
return
|
|
elif tile_y <= -self.art.height:
|
|
# TODO: handle "jump" to palette swatch, and back
|
|
#cursor.tile_y = 0
|
|
#self.popup.palette_swatch.move_cursor(cursor, 0, 0)
|
|
return
|
|
elif tile_index >= self.art.charset.last_index:
|
|
return
|
|
self.set_cursor_loc(cursor, tile_x, tile_y)
|
|
|
|
def update(self):
|
|
charset = self.ui.active_art.charset
|
|
fg, bg = self.ui.selected_fg_color, self.ui.selected_bg_color
|
|
xform = self.ui.selected_xform
|
|
# repopulate colors every update
|
|
for y in range(charset.map_height):
|
|
for x in range(charset.map_width):
|
|
self.art.set_tile_at(0, 0, x, y, None, fg, bg, xform)
|
|
self.art.update()
|
|
if self.shade_art.quad_width != self.art.quad_width or self.shade_art.quad_height != self.art.quad_height:
|
|
self.shade_art.quad_width = self.art.quad_width
|
|
self.shade_art.quad_height = self.art.quad_height
|
|
self.shade_art.geo_changed = True
|
|
self.shade_art.update()
|
|
# selection box color
|
|
elapsed_time = self.ui.app.get_elapsed_time()
|
|
color = 0.75 + (math.sin(elapsed_time / 100) / 2)
|
|
self.selection_box.color = (color, color) * 2
|
|
# set cursor color here rather than doin sin(time) again in popup update
|
|
self.popup.cursor_box.color = (color, color) * 2
|
|
# position
|
|
self.selection_box.x = self.renderable.x
|
|
selection_x = self.ui.selected_char % charset.map_width
|
|
self.selection_box.x += selection_x * self.art.quad_width
|
|
self.selection_box.y = self.renderable.y
|
|
selection_y = (self.ui.selected_char - selection_x) / charset.map_width
|
|
self.selection_box.y -= selection_y * self.art.quad_height
|
|
|
|
def render_bg(self):
|
|
# draw shaded box beneath swatch if selected color(s) too similar to BG
|
|
def is_hard_to_see(other_color_index):
|
|
return self.ui.palette.are_colors_similar(self.popup.bg_color,
|
|
self.art.palette,
|
|
other_color_index)
|
|
fg, bg = self.ui.selected_fg_color, self.ui.selected_bg_color
|
|
if is_hard_to_see(fg) or is_hard_to_see(bg):
|
|
self.shade.render()
|
|
|
|
def render(self):
|
|
if not self.popup.visible:
|
|
return
|
|
self.render_bg()
|
|
UISwatch.render(self)
|
|
self.grid.render()
|
|
self.selection_box.render()
|
|
|
|
|
|
class PaletteSwatch(UISwatch):
|
|
|
|
def reset(self):
|
|
UISwatch.reset(self)
|
|
self.transparent_x = UIRenderableX(self.ui.app, self.art)
|
|
self.fg_selection_box = SwatchSelectionBoxRenderable(self.ui.app, self.art)
|
|
self.bg_selection_box = SwatchSelectionBoxRenderable(self.ui.app, self.art)
|
|
# F label for FG color selection
|
|
self.f_art = ColorSelectionLabelArt(self.ui, 'F')
|
|
# make character dark
|
|
self.f_art.set_color_at(0, 0, 0, 0, self.f_art.palette.darkest_index, True)
|
|
self.f_renderable = ColorSelectionLabelRenderable(self.ui.app, self.f_art)
|
|
self.f_renderable.ui = self.ui
|
|
# B label for BG color seletion
|
|
self.b_art = ColorSelectionLabelArt(self.ui, 'B')
|
|
self.b_renderable = ColorSelectionLabelRenderable(self.ui.app, self.b_art)
|
|
self.b_renderable.ui = self.ui
|
|
self.renderables += self.transparent_x, self.fg_selection_box, self.bg_selection_box, self.f_renderable, self.b_renderable
|
|
|
|
def get_size(self):
|
|
# balance rows/columns according to character set swatch width
|
|
charmap_width = max(self.popup.charset_swatch.art.charset.map_width, MIN_CHARSET_WIDTH)
|
|
colors = len(self.popup.charset_swatch.art.palette.colors)
|
|
rows = math.ceil(colors / charmap_width)
|
|
columns = math.ceil(colors / rows)
|
|
# !special case hack! for atari palette
|
|
if colors == 129 and columns == 15:
|
|
columns = 16
|
|
return columns, rows
|
|
|
|
def reset_art(self):
|
|
# base our quad size on charset's
|
|
cqw, cqh = self.popup.charset_swatch.art.quad_width, self.popup.charset_swatch.art.quad_height
|
|
# maximize item size based on row/column determined in get_size()
|
|
charmap_width = max(self.art.charset.map_width, MIN_CHARSET_WIDTH)
|
|
self.art.quad_width = (charmap_width / self.art.width) * cqw
|
|
self.art.quad_height = (charmap_width / self.art.width) * cqh
|
|
self.art.clear_frame_layer(0, 0, 0)
|
|
palette = self.ui.active_art.palette
|
|
# clear color is index 0, start after that
|
|
i = 1
|
|
for y in range(self.tile_height):
|
|
for x in range(self.tile_width):
|
|
if i >= len(palette.colors):
|
|
break
|
|
self.art.set_color_at(0, 0, x, y, i, False)
|
|
i += 1
|
|
self.art.geo_changed = True
|
|
|
|
def reset_loc(self):
|
|
self.x = self.popup.x + self.popup.swatch_margin
|
|
self.y = self.popup.charset_swatch.renderable.y
|
|
# adjust Y for charset
|
|
self.y -= self.popup.charset_swatch.art.quad_height * self.ui.active_art.charset.map_height
|
|
# adjust Y for palette caption and character scale
|
|
self.y -= self.popup.art.quad_height * 2
|
|
self.renderable.x, self.renderable.y = self.x, self.y
|
|
# color 0 is always transparent, but draw it at the end
|
|
w, h = self.get_size()
|
|
colors = len(self.art.palette.colors)
|
|
if colors % w == 0:
|
|
transparent_x_tile = w - 1
|
|
elif h == 1:
|
|
transparent_x_tile = colors - 1
|
|
else:
|
|
transparent_x_tile = colors % w - 1
|
|
self.transparent_x.x = self.renderable.x
|
|
self.transparent_x.x += transparent_x_tile * self.art.quad_width
|
|
self.transparent_x.y = self.renderable.y - self.art.quad_height
|
|
self.transparent_x.y -= (h - 1) * self.art.quad_height
|
|
# set f/b_art's quad size
|
|
self.f_art.quad_width, self.f_art.quad_height = self.b_art.quad_width, self.b_art.quad_height = self.popup.art.quad_width, self.popup.art.quad_height
|
|
self.f_art.geo_changed = True
|
|
self.b_art.geo_changed = True
|
|
|
|
def is_selection_index_valid(self, index):
|
|
return index < len(self.art.palette.colors)
|
|
|
|
def set_cursor_selection_index(self, index):
|
|
# modulo wrap if selecting last color
|
|
self.popup.cursor_color = (index + 1) % len(self.art.palette.colors)
|
|
self.popup.cursor_char = -1
|
|
|
|
def move_cursor(self, cursor, dx, dy):
|
|
# similar enough to charset swatch's move_cursor, different enough to
|
|
# merit this small bit of duplicate code
|
|
pass
|
|
|
|
def update(self):
|
|
self.art.update()
|
|
self.f_art.update()
|
|
self.b_art.update()
|
|
# color selection boxes
|
|
elapsed_time = self.ui.app.get_elapsed_time()
|
|
color = 0.75 + (math.sin(elapsed_time / 100) / 2)
|
|
self.fg_selection_box.color = (color, color) * 2
|
|
self.bg_selection_box.color = (color, color) * 2
|
|
# fg selection box position
|
|
self.fg_selection_box.x = self.renderable.x
|
|
# draw transparent color last (even tho it's first in color list)
|
|
fg_x = (self.ui.selected_fg_color - 1) % self.art.width
|
|
# uneven # of palette columns, handle box specially
|
|
odd_colors = len(self.art.palette.colors) % 2 == 1
|
|
if self.ui.selected_fg_color == 0 and odd_colors:
|
|
fg_x -= 1
|
|
self.fg_selection_box.x += fg_x * self.art.quad_width
|
|
self.fg_selection_box.y = self.renderable.y
|
|
fg_y = math.floor((self.ui.selected_fg_color - 1) / self.art.width)
|
|
fg_y %= self.art.height
|
|
self.fg_selection_box.y -= fg_y * self.art.quad_height
|
|
# bg box position
|
|
self.bg_selection_box.x = self.renderable.x
|
|
bg_x = (self.ui.selected_bg_color - 1) % self.art.width
|
|
if self.ui.selected_bg_color == 0 and odd_colors:
|
|
bg_x -= 1
|
|
self.bg_selection_box.x += bg_x * self.art.quad_width
|
|
self.bg_selection_box.y = self.renderable.y
|
|
bg_y = math.floor((self.ui.selected_bg_color - 1) / self.art.width)
|
|
bg_y %= self.art.height
|
|
self.bg_selection_box.y -= bg_y * self.art.quad_height
|
|
# FG label position
|
|
self.f_renderable.alpha = 1 - color
|
|
self.f_renderable.x = self.fg_selection_box.x
|
|
self.f_renderable.y = self.fg_selection_box.y
|
|
# center F in box
|
|
x_offset = (self.art.quad_width - self.popup.art.quad_width) / 2
|
|
y_offset = (self.art.quad_height - self.popup.art.quad_height) / 2
|
|
self.f_renderable.x += x_offset
|
|
self.f_renderable.y -= y_offset
|
|
# BG label position
|
|
self.b_renderable.alpha = 1 - color
|
|
self.b_renderable.x = self.bg_selection_box.x
|
|
self.b_renderable.y = self.bg_selection_box.y
|
|
self.b_renderable.x += x_offset
|
|
self.b_renderable.y -= y_offset
|
|
|
|
def render(self):
|
|
if not self.popup.visible:
|
|
return
|
|
UISwatch.render(self)
|
|
self.transparent_x.render()
|
|
self.fg_selection_box.render()
|
|
self.bg_selection_box.render()
|
|
self.f_renderable.render()
|
|
self.b_renderable.render()
|
|
|
|
|
|
class ColorSelectionLabelArt(UIArt):
|
|
def __init__(self, ui, letter):
|
|
letter_index = ui.charset.get_char_index(letter)
|
|
art_name = '%s_%s' % (int(time.time()), self.__class__.__name__)
|
|
UIArt.__init__(self, art_name, ui.app, ui.charset, ui.palette, 1, 1)
|
|
label_color = ui.colors.white
|
|
label_bg_color = 0
|
|
self.set_tile_at(0, 0, 0, 0, letter_index, label_color, label_bg_color)
|
|
|
|
|
|
class ColorSelectionLabelRenderable(UIRenderable):
|
|
# transparent background so we can see the swatch color behind it
|
|
bg_alpha = 0
|
|
|
|
|
|
class CharacterGridRenderable(LineRenderable):
|
|
|
|
color = (0.5, 0.5, 0.5, 0.25)
|
|
|
|
def build_geo(self):
|
|
w, h = self.quad_size_ref.width, self.quad_size_ref.height
|
|
v = []
|
|
e = []
|
|
c = self.color * 4 * w * h
|
|
index = 0
|
|
for x in range(1, w):
|
|
v += [(x, -h+1), (x, 1)]
|
|
e += [index, index+1]
|
|
index += 2
|
|
for y in range(h-1):
|
|
v += [(w, -y), (0, -y)]
|
|
e += [index, index+1]
|
|
index += 2
|
|
self.vert_array = np.array(v, dtype=np.float32)
|
|
self.elem_array = np.array(e, dtype=np.uint32)
|
|
self.color_array = np.array(c, dtype=np.float32)
|