# GUI widgets and routines common to several screens. import os import math import pygame import colors import fontfx import random from constants import * # Make an outlined box. The size is given without the 4 pixel border. # This usually gets alphaed before stuff gets put in it. def make_box(color = [111, 255, 148], size = [130, 40]): s = pygame.Surface([size[0] + 8, size[1] + 8], SRCALPHA, 32) s.fill(color + [100]) r = s.get_rect() for c in [[255, 255, 255, 170], [212, 217, 255, 170], [255, 252, 255, 170], [104, 104, 104, 170]]: pygame.draw.rect(s, c, r, 1) r.width -= 2 r.height -= 2 r.top += 1 r.left += 1 return s def folder_name(name, type): if type == "mix": return name elif type == "bpm": return "%s BPM" % name else: return "%s: %s" % (type.capitalize(), name) def load_banner(filename, box = True): banner = pygame.image.load(filename) size = banner.get_size() if size <= (100, 100): # Parapara-style... no idea what to do. return banner, None elif size == (177, 135): # KSF-style 1 return banner, None elif size == (300, 200): # KSF-style 2 banner.set_colorkey(banner.get_at([0, 0]), RLEACCEL) return banner, None elif abs(size[0] - size[1]) < 3: # "Square", need to rotate. banner = banner.convert() banner.set_colorkey(banner.get_at([0, 0]), RLEACCEL) return pygame.transform.rotozoom(banner, -45, 1.0), [51, 50, 256, 80] else: # 256x80, standard banner, I hope. if size != (256, 80): banner = pygame.transform.scale(banner, [256, 80]) if box: b2 = make_box([0, 0, 0], [256, 80]) b2.blit(banner, [4, 4]) else: b2 = banner return b2, None # Just display a text string, within a specific width. class TextDisplay(pygame.sprite.Sprite): def __init__(self, font, size, midleft, str = " "): pygame.sprite.Sprite.__init__(self) self._text = str self._font = font self._size = size self._midleft = midleft self._render() def _render(self): font = fontfx.max_size(self._text, self._size[0], self._font) img = fontfx.shadow(self._text, font, [255, 255, 255]) self.image = img self.rect = self.image.get_rect() self.rect.midleft = self._midleft def set_text(self, text): self._text = text self._render() # Moving BPM display. class BPMDisplay(pygame.sprite.Sprite): def __init__(self, font, center, song = None): pygame.sprite.Sprite.__init__(self) self._last_update = pygame.time.get_ticks() self._font = font self._center = center self.set_song(song) self._render() self._color = [255, 255, 255] self._bpm_range = [0, 1] # normally [min, range] def _render(self): if self._bpm: w = 100 h = self._font.get_linesize() * 2 - self._font.get_descent() self.image = pygame.Surface([w, h], SRCALPHA, 32) self.image.fill([0, 0, 0, 0]) t1 = fontfx.shadow("BPM:", self._font, [255, 255, 255]) t2 = fontfx.shadow("%d" % int(self._bpm), self._font, self._color) r1 = t1.get_rect() r1.midtop = [50, 0] r2 = t2.get_rect() r2.midtop = [50, self._font.get_linesize()] self.image.blit(t1, r1) self.image.blit(t2, r2) else: self.image = pygame.Surface([0, 0]) self.rect = self.image.get_rect() self.rect.center = self._center def set_song(self, song): if song and "bpmdisplay" in song.info: bpms = song.info["bpmdisplay"] if bpms[0] == -1: self._bpm_idx = -1 self._bpm = 150 self._bpms = [] else: self._bpm = bpms[0] self._bpms = bpms self._bpm_idx = 1 % len(self._bpms) self._bpm_range = [min(bpms), max(max(bpms) - min(bpms), 0.00001)] if len(bpms) > 1: p = (self._bpm - self._bpm_range[0]) / self._bpm_range[1] self._color = [255 * math.sqrt(p), 255 * math.sqrt(1 - p), 0] else: self._color = [255, 255, 255] self._last_update = pygame.time.get_ticks() self._render() else: self._bpms = [] self._bpm_idx = 0 self._bpm = 0 self._last_update = pygame.time.get_ticks() self._render() def update(self, time): t = time - self._last_update if len(self._bpms) == 0: if self._bpm_idx and t > 50: self._last_update = time self._bpm = random.randrange(50, 300) self._render() elif t > 3000: self._bpm_idx = (self._bpm_idx + 1) % len(self._bpms) self._bpm = self._bpms[self._bpm_idx - 1] self._last_update = time self._render() elif t > 2000 and len(self._bpms) > 1: t -= 2000 p = t / 1000.0 self._bpm = (p * self._bpms[self._bpm_idx] + (1 - p) * self._bpms[self._bpm_idx - 1]) if len(self._bpms) > 1: # and self._bpm_range[0] > 0: p = (self._bpm - self._bpm_range[0]) / self._bpm_range[1] self._color = [255 * math.sqrt(p), 255 * math.sqrt(1 - p), 0] self._render() # Scroll an image looping vertically, from the course selection course list. class ScrollingImage(pygame.sprite.Sprite): def __init__(self, image, topleft, height): pygame.sprite.Sprite.__init__(self) self._height = height self._image = image self._topleft = topleft self.set_image(image) def set_image(self, image): if image.get_height() > self._height: self._scrolling = True self._start = pygame.time.get_ticks() + 2000 self.image = pygame.Surface([image.get_width(), self._height], SRCALPHA, 32) self._image = image self.image.blit(image, [0, 0]) self.update(pygame.time.get_ticks()) else: self._scrolling = False self.image = image self._image = None self.rect = self.image.get_rect() self.rect.topleft = self._topleft def update(self, time): if self._scrolling and self._start < time: self.image.fill([0, 0, 0, 0]) y = int(30 * ((time - self._start) / 1000.0)) y %= self._image.get_height() self.image.blit(self._image, [0, -y]) self.image.blit(self._image, [0, self._image.get_height() - y]) # Display an image. class ImageDisplay(pygame.sprite.Sprite): def __init__(self, image, topleft): pygame.sprite.Sprite.__init__(self) self._topleft = topleft self.set_image(image) def set_image(self, image): self.image = image self.rect = image.get_rect() self.rect.topleft = self._topleft # Flip an image around like it's being rotated when it changes, used on # the gameselect screen. class FlipImageDisplay(pygame.sprite.Sprite): def __init__(self, filename, center): pygame.sprite.Sprite.__init__(self) self._cache = {None: pygame.Surface([0, 0])} self._center = center self._image = self._cache[None] self._oldimage = self._cache[None] self._changed_time = pygame.time.get_ticks() - 200 self.set_image(filename) def set_image(self, filename): t = pygame.time.get_ticks() if t - self._changed_time < 200: self._changed_time = t - (200 - (t - self._changed_time)) elif t - self._changed_time < 400: self._changed_time = t - (400 - (t - self._changed_time)) else: self._changed_time = t if isinstance(filename, str) or filename is None: if filename in self._cache: self._image = self._cache[filename] else: self._image = pygame.image.load(os.path.join(image_path, filename)) self._cache[filename] = self._image else: self._image = filename def update(self, time): if time - self._changed_time > 400: self._oldimage = self._image self.image = self._image elif time - self._changed_time > 200: p = (time - self._changed_time - 200) / 200.0 x = int(p * self._image.get_width()) y = self._image.get_height() self.image = pygame.transform.scale(self._image, [x, y]) else: p = 1 - (time - self._changed_time) / 200.0 x = max(0, int(p * self._oldimage.get_width())) y = self._oldimage.get_height() self.image = pygame.transform.scale(self._oldimage, [x, y]) self.rect = self.image.get_rect() self.rect.center = self._center # Crossfading help text along the top of the screen. class HelpText(pygame.sprite.Sprite): def __init__(self, strs, color, bgcolor, font, center): pygame.sprite.Sprite.__init__(self) self._idx = -1 self._strings = [(s, font.render(s, True, color, bgcolor).convert()) for s in strs] self._center = center self._start = pygame.time.get_ticks() self._fade = -1 self._bgcolor = bgcolor self._end = -1 self.update(self._start) def update(self, time): time -= self._start # Time to switch to the next bit of text. if time > self._end: self._idx = (self._idx + 1) % len(self._strings) self.image = self._strings[self._idx][1] self.image.set_alpha(255) self._fade = time + 100 * len(self._strings[self._idx][0]) self._end = self._fade + 750 # There's a .75 second delay during which text crossfades. elif time > self._fade: p = (time - self._fade) / 750.0 s1 = self._strings[self._idx][1] s1.set_colorkey(s1.get_at([0, 0])) s1.set_alpha(int(255 * (1 - p))) i = (self._idx + 1) % len(self._strings) s2 = self._strings[i][1] s2.set_colorkey(s2.get_at([0, 0])) s2.set_alpha(int(255 * p)) h = max(s1.get_height(), s2.get_height()) w = max(s1.get_width(), s2.get_width()) self.image = pygame.Surface([w, h], 0, 32) self.image.fill(self._bgcolor) self.image.set_colorkey(self.image.get_at([0, 0])) r = s1.get_rect() r.center = self.image.get_rect().center self.image.blit(s1, r) r = s2.get_rect() r.center = self.image.get_rect().center self.image.blit(s2, r) self.image.set_colorkey(self.image.get_at([0, 0])) self.rect = self.image.get_rect() self.rect.center = self._center # Flashy indicator for showing current menu position. class ActiveIndicator(pygame.sprite.Sprite): def __init__(self, topleft, width = 220, height = 26): pygame.sprite.Sprite.__init__(self) img = pygame.image.load(os.path.join(image_path, "indicator.png")) img = img.convert() img.set_colorkey(img.get_at([0, 0])) left, mid, right = self._left_mid_right(img) bar = pygame.Surface([width, left.get_height()]) bar.blit(left, [0, 0]) bar.blit(pygame.transform.scale(mid, [width - 10, bar.get_height()]), [5, 0]) bar.blit(right, [width - 5, 0]) bar.set_colorkey(bar.get_at([0, 0])) self.image = pygame.Surface([width, height + bar.get_height()]) self.image.blit(bar, [0, 0]) self.image.blit(pygame.transform.rotate(bar, 180), [0, height]) self.image.set_colorkey(self.image.get_at([0, 0])) self.rect = self.image.get_rect() self.rect.topleft = topleft # Extract the left 5px, right 5px, and middle parts of an image. def _left_mid_right(self, img): left = pygame.Surface([5, img.get_height()]) left.blit(img, [0, 0]) right = pygame.Surface([5, img.get_height()]) right.blit(img, [5 - img.get_width(), 0]) mid = pygame.Surface([img.get_width() - 10, img.get_height()]) mid.blit(img, [-5, 0]) return left, mid, right def move(self, pt): self.rect.topleft = pt def update(self, time): self.image.set_alpha(int(255 * (0.3 + (math.sin(time / 720.0)**2 / 3.0)))) # Box to indicate the current difficulty level. class DifficultyBox(pygame.sprite.Sprite): def __init__(self, center): pygame.sprite.Sprite.__init__(self) self._topleft = [center[0]-65, center[1]-20] def set(self, diff, color, feet, grade): f = pygame.font.Font(None, 24) self.image = make_box(color) t1 = fontfx.shadow(diff, f, [255, 255, 255]) r1 = t1.get_rect() r1.center = [self.image.get_width()/2, 14] t2 = fontfx.shadow("x%d - %s" % (feet, grade), f, [255, 255, 255]) r2 = t2.get_rect() r2.center = [self.image.get_width()/2, 34] self.image.blit(t1, r1) self.image.blit(t2, r2) self.rect = self.image.get_rect() self.rect.topleft = self._topleft self.image.set_alpha(140) # Scrolling list. Used all over the place. class ListBox(pygame.sprite.Sprite): def __init__(self, font, color, spacing, count, width, topleft): pygame.sprite.Sprite.__init__(self) self._h = spacing self._count = count self._w = width self._color = color self._font = font self._topleft = topleft # animation self._start = pygame.time.get_ticks() self._animate = -1 self._animate_dir = 0 self._offset = 0 self.set_items([""]) self._needs_update = True self._render() def set_items(self, items): c2 = [c / 8 for c in self._color] self._items = [] for i in items: txt = fontfx.render_outer(i, self._w - 7, self._font) img = fontfx.shadow(txt, self._font, self._color) self._items.append(img) self._idx = self._oldidx = 0 - self._count / 2 # Reset index to 0. self._needs_update = True def set_index(self, idx, direction = 1): self._animate_dir = direction self._oldidx = self._idx self._idx = (idx - self._count / 2) self._needs_update = True def update(self, time): time -= self._start if self._idx != self._oldidx: self._animate = time + 100 if self._animate > time: self._offset = (self._animate - time) / 100.0 # 0.1 seconds self._offset *= self._h # percentage of height self._offset *= self._animate_dir # 1 for up, -1 for down self._needs_update = True elif self._offset != 0: self._offset = 0 self._needs_update = True if self._needs_update: self._oldidx = self._idx self._needs_update = False self._render() def _render(self): self.image = pygame.Surface([self._w, self._h * self._count], SRCALPHA, 32) self.image.fill([0, 0, 0, 0]) for i, y in zip(range(self._count + 2), range(-self._h / 2, self._h * (self._count + 1), self._h)): idx = (self._idx + i - 1) % len(self._items) t = self._items[idx] r = t.get_rect() r.centery = y + self._offset r.left = 5 self.image.blit(t, r) self.rect = self.image.get_rect() self.rect.topleft = self._topleft # Display the whole banner + surrounding text, with the slowly # rotating color. class BannerDisplay(pygame.sprite.Sprite): def __init__(self, center): pygame.sprite.Sprite.__init__(self) self.isfolder = False self._center = center self._clip = None self._color = [255, 0, 255] self._next_update = -1 self._delta = 5 self._idx = 1 self._bpmdisplay = BPMDisplay(pygame.font.Font(None, 24), [60, 180]) def set_song(self, song): c1 = [255, 255, 255] self._bpmdisplay.set_song(song) self._next_update = -2 # Magic value song.render() self._title = fontfx.shadow(song.info["title"], fontfx.max_size(song.info["title"], 340, 60), c1) self._r_t = self._title.get_rect() self._r_t.center = [179, 240] self._artist = fontfx.shadow(song.info["artist"], fontfx.max_size(song.info["artist"], 250, 40), c1) self._r_a = self._artist.get_rect() self._r_a.center = [179, 320] if song.info["subtitle"]: self._subtitle = fontfx.shadow(song.info["subtitle"], fontfx.max_size(song.info["subtitle"], 300, 24), c1) self._r_s = self._subtitle.get_rect() self._r_s.center = [179, 270] else: self._subtitle = None self._clip = song.clip self._banner = song.banner self._r_b = self._banner.get_rect() self._r_b.center = [179, 100] self._cdtitle = song.cdtitle self._r_cd = self._cdtitle.get_rect() self._r_cd.center = [290, 180] def _render(self): self.image = make_box(self._color, [350, 350]) self.image.blit(self._banner, self._r_b) self.image.set_clip() self.image.blit(self._title, self._r_t) self.image.blit(self._artist, self._r_a) if self._subtitle: self.image.blit(self._subtitle, self._r_s) self.image.blit(self._bpmdisplay.image, self._bpmdisplay.rect) self.image.blit(self._cdtitle, self._r_cd) self.rect = self.image.get_rect() self.rect.center = self._center def update(self, time): self._bpmdisplay.update(time) if self._next_update == -2: self._next_update = time + 300 elif time > self._next_update: self._next_update = time + 300 if ((self._delta > 0 and self._color[self._idx] == 255) or (self._delta < 0 and self._color[self._idx] == 0)): self._idx = random.choice([i for i in range(3) if i != self._idx]) if self._color[self._idx]: self._delta = -3 else: self._delta = 3 self._color[self._idx] += self._delta self._render() # Wrap some text in a sprite. class WrapTextDisplay(pygame.sprite.Sprite): def __init__(self, font, width, topleft, str = " ", centered = False): pygame.sprite.Sprite.__init__(self) self._text = str self._centered = centered self._needs_update = False self._font = fontfx.WrapFont(font, width) self._topleft = topleft self._render() def _render(self): self.image = self._font.render(self._text, shdw = False, centered = self._centered) self.rect = self.image.get_rect() self.rect.topleft = self._topleft def set_text(self, text): self._text = text self._needs_update = True def update(self, time): if self._needs_update: self._render() # The base UI screen class. A sprite list, and a background image. class InterfaceWindow(object): def __init__(self, screen, bg_fn): self._screen = screen self._bg = pygame.image.load(os.path.join(image_path, bg_fn)).convert() self._sprites = pygame.sprite.RenderUpdates() self._screen.blit(self._bg, [0, 0]) self._callbacks = {} #FIXME: TODO self._clock = pygame.time.Clock() self._time_bonus = 0 def update(self, screenshot = False): self._sprites.update(pygame.time.get_ticks() + self._time_bonus) pygame.display.update(self._sprites.draw(self._screen)) if screenshot: fn = os.path.join(rc_path, "screenshot.bmp") print "Saving a screenshot to", fn pygame.image.save(self._screen, fn) self._sprites.clear(self._screen, self._bg) self._clock.tick(45) return False NO_BANNER = os.path.join(image_path, "no-banner.png") class SongItemDisplay(object): no_banner = make_box(size = [256, 80]) tmp = pygame.image.load(NO_BANNER) tmp.set_colorkey(tmp.get_at([0, 0])) no_banner.blit(tmp, [4, 4]) def __init__(self, song): # A SongItem object self._song = song self.difficulty = song.difficulty self.info = song.info self.filename = song.filename self.diff_list = song.diff_list self.banner = None self.isfolder = False self.folder = {} self.banner = None self.clip = None def render(self): if self.banner: return elif self.info["banner"]: self.banner, self.clip = load_banner(self.info["banner"]) else: self.banner = SongItemDisplay.no_banner if self.info["cdtitle"]: self.cdtitle = pygame.image.load(self.info["cdtitle"]) else: self.cdtitle = pygame.Surface([0, 0])