playscii/ui_swatch.py

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)