diff --git a/almonds/almonds.py b/almonds/almonds.py index 745c7aa..da34dbe 100644 --- a/almonds/almonds.py +++ b/almonds/almonds.py @@ -1,8 +1,8 @@ #!/usr/bin/python # -*- encoding: utf-8 -*- -from __future__ import print_function -from __future__ import division + + import os import sys @@ -68,8 +68,8 @@ def draw_panel(cb, pool, params, plane): missing_coords = [] # Check for coordinates that have no value in current plane - xs = range(params.plane_x0, params.plane_x0 + params.plane_w - 1) - ys = range(params.plane_y0, params.plane_y0 + params.plane_h - 1) + xs = list(range(params.plane_x0, params.plane_x0 + params.plane_w - 1)) + ys = list(range(params.plane_y0, params.plane_y0 + params.plane_h - 1)) for x in xs: for y in ys: if plane[x, y] is None: @@ -262,12 +262,12 @@ def save(params): import pickle cPickle = pickle else: - import cPickle + import pickle ts = datetime.datetime.fromtimestamp(time.time()).strftime("%Y-%m-%d_%H-%M-%S") if not os.path.exists("saves/"): os.makedirs("saves/") with open("saves/almonds_%s.params" % ts, "wb") as f: - cPickle.dump(params, f) + pickle.dump(params, f) params.log("Current scene saved!") @@ -402,10 +402,10 @@ def load(path): import pickle cPickle = pickle else: - import cPickle + import pickle with open(path, "rb") as f: - params = cPickle.load(f) + params = pickle.load(f) params.log = log log("Save loaded!") return params diff --git a/almonds/almonds.py.bak b/almonds/almonds.py.bak new file mode 100644 index 0000000..745c7aa --- /dev/null +++ b/almonds/almonds.py.bak @@ -0,0 +1,539 @@ +#!/usr/bin/python +# -*- encoding: utf-8 -*- + +from __future__ import print_function +from __future__ import division + +import os +import sys +import textwrap +import multiprocessing +import subprocess + +from PIL import Image + +from .cursebox import * +from .plane import Plane +from .splash import splash +from .graphics.option_menu import * +from .graphics.input_menu import * +from .graphics.splash_popup import * +from .mandelbrot import * +from .logger import * +from .params import * +from .utils import * + +__version__ = "1.25b" + +MENU_WIDTH = 40 + + +def compute(args): + x, y, params = args + """Callable function for the multiprocessing pool.""" + return x, y, mandelbrot(x, y, params) + + +def compute_capture(args): + x, y, w, h, params = args + """Callable function for the multiprocessing pool.""" + return x, y, mandelbrot_capture(x, y, w, h, params) + + +def draw_panel(cb, pool, params, plane): + """ + Draws the application's main panel, displaying the current Mandelbrot view. + + :param cb: Cursebox instance. + :type cb: cursebox.Cursebox + :param params: Current application parameters. + :type params: params.Params + :param plane: Plane containing the current Mandelbrot values. + :type plane: plane.Plane + """ + w = cb.width - MENU_WIDTH - 1 + h = cb.height - 1 + + params.plane_w = w + params.plane_h = h + params.resize(w, h) + + palette = PALETTES[params.palette][1] + if params.reverse_palette: + palette = palette[::-1] + + # draw_gradient(t, 1, 1, w, h, palette, params.dither_type) + + generated = 0 + missing_coords = [] + + # Check for coordinates that have no value in current plane + xs = range(params.plane_x0, params.plane_x0 + params.plane_w - 1) + ys = range(params.plane_y0, params.plane_y0 + params.plane_h - 1) + for x in xs: + for y in ys: + if plane[x, y] is None: + missing_coords.append((x, y, params)) + generated += 1 + + # Compute all missing values via multiprocessing + n_processes = 0 + if len(missing_coords) > 0: + n_cores = pool._processes + n_processes = len(missing_coords) // 256 + if n_processes > n_cores: + n_processes = n_cores + + start = time.time() + for i, result in enumerate(pool.imap_unordered(compute, missing_coords, chunksize=256)): + plane[result[0], result[1]] = result[2] + if time.time() - start > 2: + if i % 200 == 0: + draw_progress_bar(cb, "Render is taking a longer time...", i, len(missing_coords)) + cb.refresh() + + if generated > 0: + params.log("Added %d missing cells" % generated) + if n_processes > 1: + params.log("(Used %d processes)" % n_processes) + + min_value = 0.0 + max_value = params.max_iterations + max_iterations = params.max_iterations + + if params.adaptive_palette: + min_value, max_value = plane.extrema(params.plane_x0, params.plane_y0, + params.plane_w, params.plane_h) + + crosshairs_coord = None + if params.crosshairs: + crosshairs_coord = params.crosshairs_coord + + # Draw all values in cursebox + for x in xs: + for y in ys: + value = (plane[x, y] + params.palette_offset) % (params.max_iterations + 1) + if params.adaptive_palette: + # Remap values from (min_value, max_value) to (0, max_iterations) + if max_value - min_value > 0: + value = ((value - min_value) / (max_value - min_value)) * max_iterations + else: + value = max_iterations + + # Dithered mode + if params.dither_type < 2: + draw_dithered_color(cb, x - params.plane_x0 + 1, + y - params.plane_y0 + 1, + palette, params.dither_type, + value, max_iterations, + crosshairs_coord=crosshairs_coord) + # 256 colors mode + else: + draw_color(cb, x - params.plane_x0 + 1, + y - params.plane_y0 + 1, + value, max_iterations, palette, + crosshairs_coord=crosshairs_coord) + + # Draw bounding box + draw_box(cb, 0, 0, w + 1, h + 1) + + +def draw_menu(cb, params, qwertz): + """ + Draws the application's side menu and options. + + :param cb: Cursebox instance. + :type cb: cursebox.Cursebox + :param params: Current application parameters. + :type params: params.Params + """ + w = cb.width + h = cb.height + + x0 = w - MENU_WIDTH + 1 + + # Clear buffer inside the box + fill(cb, x0, 1, MENU_WIDTH, h - 2, " ") + + def draw_option(key, value, shortcuts): + """ + Helper function to draw options. Self-increments own counter. + + :param key: Name of the option. + :param value: Value of the option. + :param shortcuts: Keyboard shortcut keys. + :return: + """ + draw_text(cb, x0 + 1, 2 + draw_option.counter, + "%s %s %s" % (key, str(value).rjust(MENU_WIDTH - 14 - len(key)), shortcuts)) + draw_option.counter += 1 + draw_option.counter = 1 + + z = "Z" + y = "Y" + if qwertz: + z, y = y, z + + h_seps = [2] + # Draw title + draw_text(cb, x0, 1, ("Almonds %s" % __version__).center(MENU_WIDTH - 2)) + # Write options (and stats) + # Mandelbrot position + draw_option("Real", "{0:.13g}".format(params.mb_cx), + "$[" + symbols["ARROW_LEFT"] + "]$, $[" + symbols["ARROW_RIGHT"] + "]$") + draw_option("Imaginary", "{0:.13g}".format(params.mb_cy), + "$[" + symbols["ARROW_UP"] + "]$, $[" + symbols["ARROW_DOWN"] + "]$") + # FIXME: try to find a way to avoid this hack + if is_native_windows(): + cb.put_arrow(x0 + 30, 3, "up", colors.default_bg(), colors.default_fg()) + cb.put_arrow(x0 + 35, 3, "down", colors.default_bg(), colors.default_fg()) + cb.put_arrow(x0 + 30, 4, "left", colors.default_bg(), colors.default_fg()) + cb.put_arrow(x0 + 35, 4, "right", colors.default_bg(), colors.default_fg()) + draw_option("Input coordinates...", "", "$[Enter]$") + draw_option.counter += 1 + h_seps.append(draw_option.counter + 1) + # Mandelbrot options + draw_option("Move speed", params.move_speed, "$[C]$, $[V]$") + draw_option("Zoom", "{0:.13g}".format(params.zoom), "$[" + y + "]$, $[U]$") + draw_option("Iterations", params.max_iterations, "$[I]$, $[O]$") + draw_option("Julia mode", "On" if params.julia else "Off", "$[J]$") + draw_option.counter += 1 + h_seps.append(draw_option.counter + 1) + # Palette options + draw_option("Palette", PALETTES[params.palette][0], "$[P]$") + draw_option("Color mode", DITHER_TYPES[params.dither_type][0], "$[D]$") + draw_option("Order", "Reversed" if params.reverse_palette else "Normal", "$[R]$") + draw_option("Mode", "Adaptive" if params.adaptive_palette else "Linear", "$[A]$") + draw_option("Cycle!", "", "$[" + z + "]$") + draw_option.counter += 1 + h_seps.append(draw_option.counter + 1) + # Misc. + draw_option("Hi-res capture", "", "$[H]$") + draw_option("Crosshairs", "On" if params.crosshairs else "Off", "$[X]$") + draw_option("Theme", "Dark" if colors.dark else "Light", "$[T]$") + draw_option("Save", "", "$[S]$") + draw_option("Load...", "", "$[L]$") + draw_option("Exit", "", "$[ESC]$") + + # Draw box with separators + middle = 3 + draw_option.counter + draw_box(cb, w - MENU_WIDTH, 0, MENU_WIDTH, h, h_seps=h_seps + [middle - 1, middle + 1]) + + # Draw log + draw_text(cb, x0, middle, "Event log".center(MENU_WIDTH - 2)) + latest_logs = params.log.get_latest(h - middle) + latest_logs = [textwrap.wrap(l, MENU_WIDTH - 4)[::-1] for l in latest_logs] # Wrap all messages + latest_logs = [l for ls in latest_logs for l in ls] # Flatten [[str]] -> [str] + i = h - 2 + for l in latest_logs: + draw_text(cb, x0 + 1, i, l) + i -= 1 + if i == middle + 1: + break + + +def update_display(cb, pool, params, plane, qwertz): + """ + Draws everything. + + :param cb: Cursebox instance. + :type cb: cursebox.Cursebox + :param params: Current application parameters. + :type params: params.Params + :param plane: Plane containing the current Mandelbrot values. + :type plane: plane.Plane + :return: + """ + cb.clear() + draw_panel(cb, pool, params, plane) + update_position(params) # Update Mandelbrot-space coordinates before drawing them + draw_menu(cb, params, qwertz) + cb.refresh() + + +def save(params): + """ + Saves the current parameters to a file. + + :param params: Current application parameters. + :return: + """ + if is_python3(): + import pickle + cPickle = pickle + else: + import cPickle + ts = datetime.datetime.fromtimestamp(time.time()).strftime("%Y-%m-%d_%H-%M-%S") + if not os.path.exists("saves/"): + os.makedirs("saves/") + with open("saves/almonds_%s.params" % ts, "wb") as f: + cPickle.dump(params, f) + params.log("Current scene saved!") + + +def capture(cb, pool, params): + """ + Renders and saves a screen-sized picture of the current position. + + :param cb: Cursebox instance. + :type cb: cursebox.Cursebox + :param params: Current application parameters. + :type params: params.Params + """ + w, h = screen_resolution() + + # Re-adapt dimensions to match current plane ratio + old_ratio = w / h + new_ratio = params.plane_ratio + if old_ratio > new_ratio: + w = int(h * new_ratio) + else: + h = int(w / new_ratio) + + image = Image.new("RGB", (w, h), "white") + pixels = image.load() + + # FIXME: refactor common code to get_palette(params) + palette = PALETTES[params.palette][1] + if params.reverse_palette: + palette = palette[::-1] + + # All coordinates to be computed as single arguments for processes + coords = [(x, y, w, h, params) for x in range(w) for y in range(h)] + + results = [] + # Dispatch work to pool and draw results as they come in + for i, result in enumerate(pool.imap_unordered(compute_capture, coords, chunksize=256)): + results.append(result) + if i % 2000 == 0: + draw_progress_bar(cb, "Capturing current scene...", i, w * h) + cb.refresh() + + min_value = 0.0 + max_value = params.max_iterations + max_iterations = params.max_iterations + + if params.adaptive_palette: + from operator import itemgetter + min_value = min(results, key=itemgetter(2))[2] + max_value = max(results, key=itemgetter(2))[2] + + # Draw pixels + for result in results: + value = result[2] + if params.adaptive_palette: + # Remap values from (min_value, max_value) to (0, max_iterations) + if max_value - min_value > 0: + value = ((value - min_value) / (max_value - min_value)) * max_iterations + else: + value = max_iterations + pixels[result[0], result[1]] = get_color(value, params.max_iterations, palette) + + if not os.path.exists("captures/"): + os.makedirs("captures/") + + ts = datetime.datetime.fromtimestamp(time.time()).strftime("%Y-%m-%d_%H-%M-%S") + filename = "captures/almonds_%s.png" % ts + image.save(filename, "PNG") + params.log("Current scene captured!") + params.log("(Used %d processes)" % pool._processes) + + open_file(filename) + + +def cycle(cb, pool, params, plane): + """ + Fun function to do a palette cycling animation. + + :param cb: Cursebox instance. + :type cb: cursebox.Cursebox + :param params: Current application parameters. + :type params: params.Params + :param plane: Plane containing the current Mandelbrot values. + :type plane: plane.Plane + :return: + """ + step = params.max_iterations // 20 + if step == 0: + step = 1 + for i in range(0, params.max_iterations, step): + params.palette_offset = i + draw_panel(cb, pool, params, plane) + cb.refresh() + params.palette_offset = 0 + + +def init_coords(cb, params): + """ + Initializes coordinates and zoom for first use. + + Loads coordinates from Mandelbrot-space. + + :param cb: Cursebox instance. + :type cb: cursebox.Cursebox + :param params: Current application parameters. + :type params: params.Params + :return: + """ + w = cb.width - MENU_WIDTH - 1 + h = cb.height - 1 + + params.plane_w = w + params.plane_h = h + params.resize(w, h) + + zoom(params, 1) + + +def main(pool, ratio, qwertz, savefile): + begin = time.time() + with Cursebox() as cb: + + log = Logger() + log("Welcome to Almonds %s" % __version__) + log("Exit with $[ESC]$") + log("or $[CTRL]$ + $[C]$") + + params = Params(log, ratio) + plane = Plane() + + def load(path): + if is_python3(): + import pickle + cPickle = pickle + else: + import cPickle + + with open(path, "rb") as f: + params = cPickle.load(f) + params.log = log + log("Save loaded!") + return params + + if savefile is not None: + params = load(savefile) + + popup = SplashPopup(cb, "\n".join(splash), box=True) + popup.show() + + init_coords(cb, params) + update_display(cb, pool, params, plane, qwertz) + + running = True + while running: + event = cb.poll_event().upper() + + if event == EVENT_RESIZE: + plane.reset() + elif event in (EVENT_ESC, EVENT_CTRL_C): + running = False + # Navigation + elif event == EVENT_UP: + params.plane_y0 -= 1 * params.move_speed + elif event == EVENT_DOWN: + params.plane_y0 += 1 * params.move_speed + elif event == EVENT_LEFT: + params.plane_x0 -= int(2 * params.move_speed) + elif event == EVENT_RIGHT: + params.plane_x0 += int(2 * params.move_speed) + # Move speed + elif event == "C": + params.move_speed += 1 + elif event == "V": + params.move_speed -= 1 + if params.move_speed == 0: + params.move_speed = 1 + # Manual input + elif event == EVENT_ENTER: + menu = InputMenu(cb, ["* Real (X)", "* Imaginary (Y)", " Zoom", " Iterations"], + "Input manual coordinates") + r, values = menu.show() + if r >= 0: + try: + new_mb_cx = float(values[0]) + new_mb_cy = float(values[1]) + new_zoom = params.zoom + new_iterations = params.max_iterations + try: + new_zoom = float(values[2]) + new_iterations = int(values[3]) + except ValueError: + pass + params.mb_cx = new_mb_cx + params.mb_cy = new_mb_cy + params.zoom = new_zoom + params.max_iterations = new_iterations + init_coords(cb, params) + plane.reset() + except ValueError: + params.log("Given coordinates are not floating numbers") + else: + params.log("Manual input canceled") + # Zoom / un-zoom + elif (qwertz and event == "Z") or (not qwertz and event == "Y"): + zoom(params, 1.3) + plane.reset() + elif event == "U": + zoom(params, 1 / 1.3) + plane.reset() + # Iterations control + elif event == "I": + params.max_iterations += 10 + plane.reset() + elif event == "O": + params.max_iterations -= 10 + if params.max_iterations <= 0: + params.max_iterations = 10 + else: + plane.reset() + # Palette swap + elif event == "P": + params.palette = (params.palette + 1) % len(PALETTES) + elif event == "D": + params.dither_type = (params.dither_type + 1) % len(DITHER_TYPES) + elif event == "R": + params.reverse_palette = not params.reverse_palette + # Misc + elif event == "S": + save(params) + elif event == "L": + if not os.path.exists("saves/"): + log("No saved states present") + else: + options = os.listdir("saves/") + menu = OptionMenu(cb, options, "Load save") + n = menu.show() + if n >= 0: + params = load("saves/" + options[n]) + init_coords(cb, params) + plane.reset() + else: + log("Load canceled") + elif event == "H": + capture(cb, pool, params) + elif (qwertz and event == "Y") or (not qwertz and event == "Z"): + cycle(cb, pool, params, plane) + elif event == "T": + colors.toggle_dark() + elif event == "A": + params.adaptive_palette = not params.adaptive_palette + elif event == "J": + params.toggle_julia() + init_coords(cb, params) + plane.reset() + elif event == "X": + params.crosshairs = not params.crosshairs + + if running: + update_display(cb, pool, params, plane, qwertz) + + spent = (time.time() - begin) // 60 + spaces = " " * 26 + if not(is_native_windows()): + print("\n".join(splash)) + else: + os.system("cls") + spaces = "" + print() + print("%sSpent %d minute%s exploring fractals, see you soon :)\n" % (spaces, spent, "s" if spent > 1 else "")) + print("%s- Almonds %s by Tenchi " % (spaces, __version__)) diff --git a/almonds/cursebox/colortrans.py b/almonds/cursebox/colortrans.py index a6f8a88..3f86b63 100644 --- a/almonds/cursebox/colortrans.py +++ b/almonds/cursebox/colortrans.py @@ -286,7 +286,7 @@ def _str2hex(hexstr): def _create_dicts(): short2rgb_dict = dict(CLUT) rgb2short_dict = {} - for k, v in short2rgb_dict.items(): + for k, v in list(short2rgb_dict.items()): rgb2short_dict[v] = k return rgb2short_dict, short2rgb_dict diff --git a/almonds/cursebox/colortrans.py.bak b/almonds/cursebox/colortrans.py.bak new file mode 100644 index 0000000..a6f8a88 --- /dev/null +++ b/almonds/cursebox/colortrans.py.bak @@ -0,0 +1,331 @@ +# -*- encoding: utf-8 -*- + +# Adapted from https://gist.github.com/MicahElliott/719710 + +__author__ = 'Micah Elliott http://MicahElliott.com' +__version__ = '0.1' +__copyright__ = 'Copyright (C) 2011 Micah Elliott. All rights reserved.' +__license__ = 'WTFPL http://sam.zoy.org/wtfpl/' + +import sys +import re + + +CLUT = [ # color look-up table + # 8-bit, RGB hex + + # Primary 3-bit (8 colors). Unique representation! + ('00', '000000'), + ('01', '800000'), + ('02', '008000'), + ('03', '808000'), + ('04', '000080'), + ('05', '800080'), + ('06', '008080'), + ('07', 'c0c0c0'), + + # Equivalent "bright" versions of original 8 colors. + ('08', '808080'), + ('09', 'ff0000'), + ('10', '00ff00'), + ('11', 'ffff00'), + ('12', '0000ff'), + ('13', 'ff00ff'), + ('14', '00ffff'), + ('15', 'ffffff'), + + # Strictly ascending. + ('16', '000000'), + ('17', '00005f'), + ('18', '000087'), + ('19', '0000af'), + ('20', '0000d7'), + ('21', '0000ff'), + ('22', '005f00'), + ('23', '005f5f'), + ('24', '005f87'), + ('25', '005faf'), + ('26', '005fd7'), + ('27', '005fff'), + ('28', '008700'), + ('29', '00875f'), + ('30', '008787'), + ('31', '0087af'), + ('32', '0087d7'), + ('33', '0087ff'), + ('34', '00af00'), + ('35', '00af5f'), + ('36', '00af87'), + ('37', '00afaf'), + ('38', '00afd7'), + ('39', '00afff'), + ('40', '00d700'), + ('41', '00d75f'), + ('42', '00d787'), + ('43', '00d7af'), + ('44', '00d7d7'), + ('45', '00d7ff'), + ('46', '00ff00'), + ('47', '00ff5f'), + ('48', '00ff87'), + ('49', '00ffaf'), + ('50', '00ffd7'), + ('51', '00ffff'), + ('52', '5f0000'), + ('53', '5f005f'), + ('54', '5f0087'), + ('55', '5f00af'), + ('56', '5f00d7'), + ('57', '5f00ff'), + ('58', '5f5f00'), + ('59', '5f5f5f'), + ('60', '5f5f87'), + ('61', '5f5faf'), + ('62', '5f5fd7'), + ('63', '5f5fff'), + ('64', '5f8700'), + ('65', '5f875f'), + ('66', '5f8787'), + ('67', '5f87af'), + ('68', '5f87d7'), + ('69', '5f87ff'), + ('70', '5faf00'), + ('71', '5faf5f'), + ('72', '5faf87'), + ('73', '5fafaf'), + ('74', '5fafd7'), + ('75', '5fafff'), + ('76', '5fd700'), + ('77', '5fd75f'), + ('78', '5fd787'), + ('79', '5fd7af'), + ('80', '5fd7d7'), + ('81', '5fd7ff'), + ('82', '5fff00'), + ('83', '5fff5f'), + ('84', '5fff87'), + ('85', '5fffaf'), + ('86', '5fffd7'), + ('87', '5fffff'), + ('88', '870000'), + ('89', '87005f'), + ('90', '870087'), + ('91', '8700af'), + ('92', '8700d7'), + ('93', '8700ff'), + ('94', '875f00'), + ('95', '875f5f'), + ('96', '875f87'), + ('97', '875faf'), + ('98', '875fd7'), + ('99', '875fff'), + ('100', '878700'), + ('101', '87875f'), + ('102', '878787'), + ('103', '8787af'), + ('104', '8787d7'), + ('105', '8787ff'), + ('106', '87af00'), + ('107', '87af5f'), + ('108', '87af87'), + ('109', '87afaf'), + ('110', '87afd7'), + ('111', '87afff'), + ('112', '87d700'), + ('113', '87d75f'), + ('114', '87d787'), + ('115', '87d7af'), + ('116', '87d7d7'), + ('117', '87d7ff'), + ('118', '87ff00'), + ('119', '87ff5f'), + ('120', '87ff87'), + ('121', '87ffaf'), + ('122', '87ffd7'), + ('123', '87ffff'), + ('124', 'af0000'), + ('125', 'af005f'), + ('126', 'af0087'), + ('127', 'af00af'), + ('128', 'af00d7'), + ('129', 'af00ff'), + ('130', 'af5f00'), + ('131', 'af5f5f'), + ('132', 'af5f87'), + ('133', 'af5faf'), + ('134', 'af5fd7'), + ('135', 'af5fff'), + ('136', 'af8700'), + ('137', 'af875f'), + ('138', 'af8787'), + ('139', 'af87af'), + ('140', 'af87d7'), + ('141', 'af87ff'), + ('142', 'afaf00'), + ('143', 'afaf5f'), + ('144', 'afaf87'), + ('145', 'afafaf'), + ('146', 'afafd7'), + ('147', 'afafff'), + ('148', 'afd700'), + ('149', 'afd75f'), + ('150', 'afd787'), + ('151', 'afd7af'), + ('152', 'afd7d7'), + ('153', 'afd7ff'), + ('154', 'afff00'), + ('155', 'afff5f'), + ('156', 'afff87'), + ('157', 'afffaf'), + ('158', 'afffd7'), + ('159', 'afffff'), + ('160', 'd70000'), + ('161', 'd7005f'), + ('162', 'd70087'), + ('163', 'd700af'), + ('164', 'd700d7'), + ('165', 'd700ff'), + ('166', 'd75f00'), + ('167', 'd75f5f'), + ('168', 'd75f87'), + ('169', 'd75faf'), + ('170', 'd75fd7'), + ('171', 'd75fff'), + ('172', 'd78700'), + ('173', 'd7875f'), + ('174', 'd78787'), + ('175', 'd787af'), + ('176', 'd787d7'), + ('177', 'd787ff'), + ('178', 'd7af00'), + ('179', 'd7af5f'), + ('180', 'd7af87'), + ('181', 'd7afaf'), + ('182', 'd7afd7'), + ('183', 'd7afff'), + ('184', 'd7d700'), + ('185', 'd7d75f'), + ('186', 'd7d787'), + ('187', 'd7d7af'), + ('188', 'd7d7d7'), + ('189', 'd7d7ff'), + ('190', 'd7ff00'), + ('191', 'd7ff5f'), + ('192', 'd7ff87'), + ('193', 'd7ffaf'), + ('194', 'd7ffd7'), + ('195', 'd7ffff'), + ('196', 'ff0000'), + ('197', 'ff005f'), + ('198', 'ff0087'), + ('199', 'ff00af'), + ('200', 'ff00d7'), + ('201', 'ff00ff'), + ('202', 'ff5f00'), + ('203', 'ff5f5f'), + ('204', 'ff5f87'), + ('205', 'ff5faf'), + ('206', 'ff5fd7'), + ('207', 'ff5fff'), + ('208', 'ff8700'), + ('209', 'ff875f'), + ('210', 'ff8787'), + ('211', 'ff87af'), + ('212', 'ff87d7'), + ('213', 'ff87ff'), + ('214', 'ffaf00'), + ('215', 'ffaf5f'), + ('216', 'ffaf87'), + ('217', 'ffafaf'), + ('218', 'ffafd7'), + ('219', 'ffafff'), + ('220', 'ffd700'), + ('221', 'ffd75f'), + ('222', 'ffd787'), + ('223', 'ffd7af'), + ('224', 'ffd7d7'), + ('225', 'ffd7ff'), + ('226', 'ffff00'), + ('227', 'ffff5f'), + ('228', 'ffff87'), + ('229', 'ffffaf'), + ('230', 'ffffd7'), + ('231', 'ffffff'), + + # Gray-scale range. + ('232', '080808'), + ('233', '121212'), + ('234', '1c1c1c'), + ('235', '262626'), + ('236', '303030'), + ('237', '3a3a3a'), + ('238', '444444'), + ('239', '4e4e4e'), + ('240', '585858'), + ('241', '626262'), + ('242', '6c6c6c'), + ('243', '767676'), + ('244', '808080'), + ('245', '8a8a8a'), + ('246', '949494'), + ('247', '9e9e9e'), + ('248', 'a8a8a8'), + ('249', 'b2b2b2'), + ('250', 'bcbcbc'), + ('251', 'c6c6c6'), + ('252', 'd0d0d0'), + ('253', 'dadada'), + ('254', 'e4e4e4'), + ('255', 'eeeeee')] + + +def _str2hex(hexstr): + return int(hexstr, 16) + + +def _create_dicts(): + short2rgb_dict = dict(CLUT) + rgb2short_dict = {} + for k, v in short2rgb_dict.items(): + rgb2short_dict[v] = k + return rgb2short_dict, short2rgb_dict + + +def short2rgb(short): + return SHORT2RGB_DICT[short] + + +def rgb2short(rgb): + """ Find the closest xterm-256 approximation to the given RGB value. + @param rgb: Hex code representing an RGB value, eg, 'abcdef' + @returns: String between 0 and 255, compatible with xterm. + >>> rgb2short('123456') + ('23', '005f5f') + >>> rgb2short('ffffff') + ('231', 'ffffff') + >>> rgb2short('0DADD6') # vimeo logo + ('38', '00afd7') + """ + incs = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff) + # Break 6-char RGB code into 3 integer vals. + parts = [ int(h, 16) for h in re.split(r'(..)(..)(..)', rgb)[1:4] ] + res = [] + for part in parts: + i = 0 + while i < len(incs)-1: + s, b = incs[i], incs[i+1] # smaller, bigger + if s <= part <= b: + s1 = abs(s - part) + b1 = abs(b - part) + if s1 < b1: closest = s + else: closest = b + res.append(closest) + break + i += 1 + #print '***', res + res = ''.join([ ('%02.x' % i) for i in res ]) + equiv = RGB2SHORT_DICT[ res ] + #print '***', res, equiv + return equiv, res + +RGB2SHORT_DICT, SHORT2RGB_DICT = _create_dicts() diff --git a/almonds/cursebox/symbols.py b/almonds/cursebox/symbols.py index 523166a..07ea4f4 100644 --- a/almonds/cursebox/symbols.py +++ b/almonds/cursebox/symbols.py @@ -6,23 +6,23 @@ from ..utils import is_native_windows UTF8_SYMBOLS = { - "BOX_TOP_LEFT" : u"┌", - "BOX_TOP_RIGHT" : u"┐", - "BOX_BOTTOM_LEFT" : u"└", - "BOX_BOTTOM_RIGHT": u"┘", - "BOX_HORIZONTAL" : u"─", - "BOX_VERTICAL" : u"│", - "BOX_X_LEFT" : u"├", - "BOX_X_RIGHT" : u"┤", - "BOX_X_TOP" : u"┬", - "BOX_X_BOTTOM" : u"┴", - "BOX_X_MIDDLE" : u"┼", - "DITHER_1" : u"█▓▒░ ", - "DITHER_2" : u"#&X$x=+;:,. ", - "ARROW_UP" : u"↑", - "ARROW_DOWN" : u"↓", - "ARROW_LEFT" : u"←", - "ARROW_RIGHT" : u"→" + "BOX_TOP_LEFT" : "┌", + "BOX_TOP_RIGHT" : "┐", + "BOX_BOTTOM_LEFT" : "└", + "BOX_BOTTOM_RIGHT": "┘", + "BOX_HORIZONTAL" : "─", + "BOX_VERTICAL" : "│", + "BOX_X_LEFT" : "├", + "BOX_X_RIGHT" : "┤", + "BOX_X_TOP" : "┬", + "BOX_X_BOTTOM" : "┴", + "BOX_X_MIDDLE" : "┼", + "DITHER_1" : "█▓▒░ ", + "DITHER_2" : "#&X$x=+;:,. ", + "ARROW_UP" : "↑", + "ARROW_DOWN" : "↓", + "ARROW_LEFT" : "←", + "ARROW_RIGHT" : "→" } CP437_SYMBOLS = { diff --git a/almonds/cursebox/symbols.py.bak b/almonds/cursebox/symbols.py.bak new file mode 100644 index 0000000..523166a --- /dev/null +++ b/almonds/cursebox/symbols.py.bak @@ -0,0 +1,75 @@ +# -*- encoding: utf-8 -*- + +import os +import sys + +from ..utils import is_native_windows + +UTF8_SYMBOLS = { + "BOX_TOP_LEFT" : u"┌", + "BOX_TOP_RIGHT" : u"┐", + "BOX_BOTTOM_LEFT" : u"└", + "BOX_BOTTOM_RIGHT": u"┘", + "BOX_HORIZONTAL" : u"─", + "BOX_VERTICAL" : u"│", + "BOX_X_LEFT" : u"├", + "BOX_X_RIGHT" : u"┤", + "BOX_X_TOP" : u"┬", + "BOX_X_BOTTOM" : u"┴", + "BOX_X_MIDDLE" : u"┼", + "DITHER_1" : u"█▓▒░ ", + "DITHER_2" : u"#&X$x=+;:,. ", + "ARROW_UP" : u"↑", + "ARROW_DOWN" : u"↓", + "ARROW_LEFT" : u"←", + "ARROW_RIGHT" : u"→" +} + +CP437_SYMBOLS = { + "BOX_TOP_LEFT" : chr(218), + "BOX_TOP_RIGHT" : chr(191), + "BOX_BOTTOM_LEFT" : chr(192), + "BOX_BOTTOM_RIGHT": chr(217), + "BOX_HORIZONTAL" : chr(196), + "BOX_VERTICAL" : chr(179), + "BOX_X_LEFT" : chr(195), + "BOX_X_RIGHT" : chr(180), + "BOX_X_TOP" : chr(194), + "BOX_X_BOTTOM" : chr(193), + "BOX_X_MIDDLE" : chr(197), + "DITHER_1" : chr(219) + chr(178) + chr(177) + chr(176) + chr(32), + "DITHER_2" : "#&X$x=+;:,. ", + "ARROW_UP" : chr(24), + "ARROW_DOWN" : chr(25), + "ARROW_LEFT" : chr(27), + "ARROW_RIGHT" : chr(26) +} + + +class Symbols(object): + def __init__(self): + if is_native_windows(): + self.symbols = CP437_SYMBOLS + self.encoding = "cp437" + else: + self.symbols = UTF8_SYMBOLS + self.encoding = "utf-8" + self.symbols["DITHER_1"] = list(self.symbols["DITHER_1"]) + + def __getitem__(self, key): + return self.symbols[key] + + @property + def dither1(self): + return self.symbols["DITHER_1"] + + @property + def dither2(self): + return self.symbols["DITHER_2"] + + def encode(self, string): + if self.encoding == "cp437": + return string + return string.encode(self.encoding) + +symbols = Symbols() diff --git a/almonds/graphics/drawing.py b/almonds/graphics/drawing.py index d934a87..6b0aec6 100644 --- a/almonds/graphics/drawing.py +++ b/almonds/graphics/drawing.py @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- -from __future__ import division + import math import os @@ -181,7 +181,7 @@ def draw_scroll_bar(cb, x0, y0, h, n_visible, n_items, position, fg=colors.defau knob_end = knob_position + knob_height for y in range(h): - symbol = u"█" if knob_position <= y <= knob_end else u"░" + symbol = "█" if knob_position <= y <= knob_end else "░" cb.put(x0, y0 + y, symbol, fg(), bg()) diff --git a/almonds/graphics/drawing.py.bak b/almonds/graphics/drawing.py.bak new file mode 100644 index 0000000..d934a87 --- /dev/null +++ b/almonds/graphics/drawing.py.bak @@ -0,0 +1,215 @@ +# -*- encoding: utf-8 -*- + +from __future__ import division + +import math +import os +import sys + +from ..cursebox import colors +from ..cursebox import symbols +from ..utils import * + +# Box drawing symbols + +BOX_CORNERS = [symbols["BOX_TOP_LEFT"], symbols["BOX_TOP_RIGHT"], + symbols["BOX_BOTTOM_LEFT"], symbols["BOX_BOTTOM_RIGHT"]] + +# Block drawing symbols + +DITHER_TYPES = [("8 colors ANSI", symbols.dither1), + ("8 colors ASCII", symbols.dither2), + ["256 colors"]] + +if is_native_windows(): + DITHER_TYPES.pop() + +# Palettes + +PALETTE_1 = [colors.black, colors.blue, colors.cyan, colors.white] +PALETTE_2 = [colors.black, colors.red, colors.yellow, colors.black] +PALETTE_3 = [colors.black, colors.green, colors.cyan, colors.yellow, colors.white] +PALETTE_4 = [colors.white, colors.black, colors.white] +PALETTE_5 = [colors.black, colors.blue, colors.yellow, colors.white] +PALETTE_6 = [colors.white, colors.magenta, colors.black, colors.black, colors.white] +PALETTE_7 = [colors.black, colors.red, colors.yellow, colors.green, + colors.cyan, colors.blue, colors.magenta, colors.black] +PALETTE_8 = [colors.black, colors.red, colors.yellow] +PALETTES = [("Moonlight", PALETTE_1), + ("Magma", PALETTE_2), + ("Radioactive", PALETTE_3), + ("Monochrome", PALETTE_4), + ("Neon", PALETTE_5), + ("Hello Kitty", PALETTE_6), + ("Rainbow", PALETTE_7), + ("Fire", PALETTE_8)] + +# Colors + +COLORS = {colors.black: ( 0, 0, 0), + colors.red: (255, 0, 0), + colors.green: ( 0, 255, 0), + colors.blue: ( 0, 0, 255), + colors.yellow: (255, 255, 0), + colors.magenta: (255, 0, 255), + colors.cyan: (0, 255, 255), + colors.white: (255, 255, 255)} + + +def sort_palette(palette): + def intensity(color): + return sum(COLORS[color]) / 3 + + return sorted(list(set(palette)), key=intensity) + + +def dither_symbol(value, dither): + """ + Returns the appropriate block drawing symbol for the given intensity. + :param value: intensity of the color, in the range [0.0, 1.0] + :return: dithered symbol representing that intensity + """ + dither = DITHER_TYPES[dither][1] + return dither[int(round(value * (len(dither) - 1)))] + + +def draw_dithered_color(cb, x, y, palette, dither, n, n_max, crosshairs_coord=None): + """ + Draws a dithered color block on the terminal, given a palette. + :type cb: cursebox.CurseBox + """ + i = n * (len(palette) - 1) / n_max + c1 = palette[int(math.floor(i))] + c2 = palette[int(math.ceil(i))] + value = i - int(math.floor(i)) + + symbol = dither_symbol(value, dither) + + if crosshairs_coord is not None: + old_symbol = symbol + symbol, crosshairs = get_crosshairs_symbol(x, y, old_symbol, crosshairs_coord) + if crosshairs: + sorted_palette = sort_palette(palette) + if old_symbol == DITHER_TYPES[dither][1][0]: + c2 = c1 + sorted_index = sorted_palette.index(c2) + if sorted_index > len(sorted_palette) // 2: + c1 = sorted_palette[0] + else: + c1 = sorted_palette[-1] + + cb.put(x, y, symbol, c1(), c2()) + + +def draw_color(cb, x, y, value, max_iterations, palette, crosshairs_coord=None): + bg = get_color(value, max_iterations, palette) + symbol = " " + fg = colors.black() + if crosshairs_coord is not None: + symbol, crosshairs = get_crosshairs_symbol(x, y, symbol, crosshairs_coord) + if crosshairs: + fg = colors.to_xterm((255 - bg[0], 255 - bg[1], 255 - bg[2])) + + cb.put(x, y, symbol, fg, colors.to_xterm(bg)) + + +def get_crosshairs_symbol(x, y, symbol, crosshairs_coord): + + crosshairs = False + if x == crosshairs_coord[0]: + crosshairs = True + if y == crosshairs_coord[1]: + symbol = symbols["BOX_X_MIDDLE"] + else: + symbol = symbols["BOX_VERTICAL"] + elif y == crosshairs_coord[1]: + crosshairs = True + symbol = symbols["BOX_HORIZONTAL"] + + return symbol, crosshairs + + +def draw_box(cb, x0, y0, w, h, fg=colors.default_fg, bg=colors.default_bg, h_seps=[], v_seps=[]): + """ + Draws a box in the given terminal. + :type cb: cursebox.CurseBox + """ + w -= 1 + h -= 1 + corners = [(x0, y0), (x0 + w, y0), (x0, y0 + h), (x0 + w, y0 + h)] + + fg = fg() + bg = bg() + + for i, c in enumerate(corners): + cb.put(c[0], c[1], BOX_CORNERS[i], fg, bg) + + for s in h_seps + [0, h]: + cb.put(x0 + 1, y0 + s, symbols["BOX_HORIZONTAL"] * (w - 1), fg, bg) + + for y in range(1, h): + for s in v_seps + [0, w]: + cb.put(x0 + s, y0 + y, symbols["BOX_VERTICAL"], fg, bg) + + for s in h_seps: + cb.put(x0, y0 + s, symbols["BOX_X_LEFT"], fg, bg) + cb.put(x0 + w, y0 + s, symbols["BOX_X_RIGHT"], fg, bg) + + for s in v_seps: + cb.put(x0 + s, y0, symbols["BOX_X_TOP"], fg, bg) + cb.put(x0 + s, y0 + h, symbols["BOX_X_BOTTOM"], fg, bg) + + +def draw_progress_bar(cb, message, value, max_value): + """ + :type cb: cursebox.Cursebox + """ + m_x = cb.width // 2 + m_y = cb.height // 2 + w = len(message) + 4 + h = 3 + draw_box(cb, m_x - w // 2, m_y - 1, w, h) + message = " %s " % message + i = int((value / max_value) * (len(message) + 2)) + message = "$" + message[:i] + "$" + message[i:] + draw_text(cb, m_x - w // 2 + 1, m_y, message) + + +def draw_scroll_bar(cb, x0, y0, h, n_visible, n_items, position, fg=colors.default_fg, bg=colors.default_bg): + knob_height = int(h * n_visible / n_items) + knob_position = int((h - knob_height) * position / n_items) + knob_end = knob_position + knob_height + + for y in range(h): + symbol = u"█" if knob_position <= y <= knob_end else u"░" + cb.put(x0, y0 + y, symbol, fg(), bg()) + + +def draw_text(cb, x0, y0, string, fg=colors.default_fg, bg=colors.default_bg): + markup_compensation = 0 + fg = fg() + bg = bg() + for i, c in enumerate(string): + if c == "$": + fg, bg = bg, fg + markup_compensation += 1 + continue + cb.put(x0 + i - markup_compensation, y0, c, fg, bg) + + +def fill(cb, x0, y0, w, h, symbol, fg=colors.default_fg, bg=colors.default_bg): + for y in range(h): + cb.put(x0, y0 + y, symbol * (w - 1), fg(), bg()) + + +def interpolate(c1, c2, factor): + return (int(c1[0] * (1 - factor) + c2[0] * factor), + int(c1[1] * (1 - factor) + c2[1] * factor), + int(c1[2] * (1 - factor) + c2[2] * factor)) + + +def get_color(count, max_iterations, palette): + i = count * (len(palette) - 1.0) / max_iterations + c1 = COLORS[palette[int(math.floor(i))]] + c2 = COLORS[palette[int(math.ceil(i))]] + return interpolate(c1, c2, i - int(math.floor(i))) diff --git a/almonds/graphics/input_menu.py b/almonds/graphics/input_menu.py index af638d1..223f93a 100644 --- a/almonds/graphics/input_menu.py +++ b/almonds/graphics/input_menu.py @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- -from __future__ import division + from ..cursebox import * from .drawing import * diff --git a/almonds/graphics/input_menu.py.bak b/almonds/graphics/input_menu.py.bak new file mode 100644 index 0000000..af638d1 --- /dev/null +++ b/almonds/graphics/input_menu.py.bak @@ -0,0 +1,113 @@ +# -*- encoding: utf-8 -*- + +from __future__ import division + +from ..cursebox import * +from .drawing import * + + +class InputMenu(object): + + INPUT_WIDTH = 20 + + def __init__(self, cb, fields, title=""): + self.cb = cb + + self.fields = fields + self.values = ["" for _ in fields] + self.title = title + self.line = 0 + self.column = 0 + self.status = 0 + self.longest = 0 + + self.width = 0 + self.height = 0 + self.x0 = 0 + self.y0 = 0 + + self.open = True + + def show(self): + self.draw_inputs() + + while self.open: + event = self.cb.poll_event() + + if event == EVENT_ESC: + self.status = -1 + self.open = False + + if event in (EVENT_UP, EVENT_DOWN): + if event == EVENT_UP: + self.line = (self.line - 1) % len(self.fields) + elif event == EVENT_DOWN: + self.line = (self.line + 1) % len(self.fields) + self.column = len(self.values[self.line]) + elif event == EVENT_ENTER: + self.open = False + elif event == EVENT_BACKSPACE: + if self.column > 0: + self.column -= 1 + self.values[self.line] = self.values[self.line][:-1] + elif self.column < self.INPUT_WIDTH: + if len(event) == 1: + self.column += 1 + self.values[self.line] += event + + if self.open: + self.draw_inputs() + + self.cb.hide_cursor() + + return self.status, self.values + + def draw_inputs(self): + self.update_dimensions() + + # Clear + fill(self.cb, self.x0 + 1, self.y0 + 1, self.width - 2, self.height - 2, " ") + + offset_y = 0 # Vertical offset if a title is present + + # If there's a title, add a separator in the box + h_seps = [] + if self.title != "": + offset_y = 2 + h_seps.append(2) + # Draw box, with eventual separator + draw_box(self.cb, self.x0, self.y0, self.width, self.height, h_seps=h_seps) + + # Centered title + draw_text(self.cb, self.x0 + 1, self.y0 + 1, " " * ((self.width - 2 - len(self.title)) // 2) + self.title) + + for y, field in enumerate(self.fields): + text = " " + field + ": " + " " * (self.longest - len(field)) + if self.line == y: + text += "$" + text += self.values[y] + " " * (self.INPUT_WIDTH - len(self.values[y])) + + draw_text(self.cb, self.x0 + 1, self.y0 + offset_y + y + 1, text) + + self.cb.set_cursor(self.x0 + self.longest + 4 + self.column, self.y0 + offset_y + self.line + 1) + + self.cb.refresh() + + def update_dimensions(self): + # Prevent menu from taking the whole screen + max_width = 2 * self.cb.width // 5 + + longest = len(max(self.fields, key=len)) + self.longest = longest + # Fit menu to option lengths if small enough, else use proportions + # Also add space for borders, spaces, fields, ": "... + self.width = (max_width if longest >= max_width else longest) + 4 + self.INPUT_WIDTH + 2 + self.height = len(self.fields) + 2 + + # If a title is displayed, add more height + if self.title != "": + self.height += 2 + + # Center the window + self.x0 = (self.cb.width - self.width) // 2 + self.y0 = (self.cb.height - self.height) // 2 diff --git a/almonds/graphics/option_menu.py b/almonds/graphics/option_menu.py index deb54bc..0632aad 100644 --- a/almonds/graphics/option_menu.py +++ b/almonds/graphics/option_menu.py @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- -from __future__ import division + from ..cursebox import * from .drawing import * diff --git a/almonds/graphics/option_menu.py.bak b/almonds/graphics/option_menu.py.bak new file mode 100644 index 0000000..deb54bc --- /dev/null +++ b/almonds/graphics/option_menu.py.bak @@ -0,0 +1,119 @@ +# -*- encoding: utf-8 -*- + +from __future__ import division + +from ..cursebox import * +from .drawing import * + + +class OptionMenu(object): + def __init__(self, cb, options, title=""): + self.cb = cb + + self.title = title + self.options = options + self.selected = 0 + self.open = True + + self.width = 0 + self.height = 0 + self.x0 = 0 + self.y0 = 0 + + def show(self): + self.draw_menu() + + while self.open: + event = self.cb.poll_event() + if event == EVENT_ESC: + self.selected = -1 + self.open = False + elif event == EVENT_UP: + self.selected = (self.selected - 1) % len(self.options) + elif event == EVENT_DOWN: + self.selected = (self.selected + 1) % len(self.options) + elif event == EVENT_ENTER: + self.open = False + + if self.open: + self.draw_menu() + + return self.selected + + def draw_menu(self): + self.update_dimensions() + + # Clear + fill(self.cb, self.x0 + 1, self.y0 + 1, self.width - 2, self.height - 2, " ") + + offset_y = 0 # Vertical offset if a title is present + offset_x = 0 # Horizontal negative offset if a scroll bar is present + + # If there's a title, add a separator in the box + h_seps = [] + if self.title != "": + offset_y = 2 + h_seps.append(2) + # Draw box, with eventual separator + draw_box(self.cb, self.x0, self.y0, self.width, self.height, h_seps=h_seps) + + # Centered title + draw_text(self.cb, self.x0 + 1, self.y0 + 1, " " * ((self.width - 2 - len(self.title)) // 2) + self.title) + + # Figure out if we need to limit the view due to too many options + view = self.options + max_items = self.height - offset_y - 2 + offset_selected = 0 + # If too many options.. + if len(view) > max_items: + # If selected option is further than the first half of first visible part the list, + if self.selected > max_items // 2: + # Start offsetting the view + offset_selected = self.selected - (max_items // 2) + # If we reach the end of the list, + if self.selected > len(view) - (max_items // 2): + # Keep a fixed offset + offset_selected = len(view) - max_items + + view = view[offset_selected:max_items + offset_selected] + offset_x = 1 + + # Draw all options + for y, item in enumerate(view): + text = " " + item # Prepend a space for visual prettiness + if len(text) < self.width - 2 - offset_x: # If option too short, + text += " " * (self.width - 2 - len(text) - offset_x) # add spaces for highlight when selected + elif len(text) > self.width - 3 - offset_x: # If too long, + text = text[:self.width - 6 - offset_x] + "... " # truncate + if y == self.selected - offset_selected: # If it's the selected option, + text = "$" + text # highlight using custom markup + + draw_text(self.cb, self.x0 + 1, self.y0 + offset_y + y + 1, text) + + # Draw scrollbar if present + if offset_x != 0: + draw_scroll_bar(self.cb, self.x0 + self.width - 2, self.y0 + 1 + offset_y, self.height - 2 - offset_y, + max_items, len(self.options), self.selected) + + self.cb.refresh() + + def update_dimensions(self): + # Prevent menu from taking the whole screen + max_width = 2 * self.cb.width // 5 + max_height = 2 * self.cb.height // 3 + + longest = len(max(self.options, key=len)) + # Fit menu to option lengths if small enough, else use proportions + self.width = (max_width if longest >= max_width else longest) + 4 + self.height = (max_height if len(self.options) >= max_height else len(self.options)) + 2 + + # If a title is displayed, add more height + if self.title != "": + self.height += 2 + # If a scroll bar will be displayed, add more width + if len(self.options) > self.height - (4 if self.title == "" else 2): + self.width += 1 + + # Center the menu + self.x0 = (self.cb.width - self.width) // 2 + self.y0 = (self.cb.height - self.height) // 2 diff --git a/almonds/graphics/splash_popup.py b/almonds/graphics/splash_popup.py index 60c879b..83bc204 100644 --- a/almonds/graphics/splash_popup.py +++ b/almonds/graphics/splash_popup.py @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- -from __future__ import division + from time import sleep diff --git a/almonds/graphics/splash_popup.py.bak b/almonds/graphics/splash_popup.py.bak new file mode 100644 index 0000000..60c879b --- /dev/null +++ b/almonds/graphics/splash_popup.py.bak @@ -0,0 +1,48 @@ +# -*- encoding: utf-8 -*- + +from __future__ import division + +from time import sleep + +from .drawing import * + + +class SplashPopup(object): + def __init__(self, cb, message, box=False): + self.cb = cb + + self.message = message.splitlines() + self.box = box + + self.width = 0 + self.height = 0 + self.x0 = 0 + self.y0 = 0 + + def show(self): + self.draw_popup() + sleep(1) + return + + def draw_popup(self): + self.update_dimensions() + + # Clear + fill(self.cb, 0, 0, self.cb.width, self.cb.height, " ", + colors.black, lambda: 16) + + if self.box: + draw_box(self.cb, self.x0, self.y0, self.width, self.height) + + for y, line in enumerate(self.message): + draw_text(self.cb, self.x0 + 1, self.y0 + 1 + y, line) + + self.cb.refresh() + + def update_dimensions(self): + self.width = len(self.message[0]) + 2 + self.height = len(self.message) + 2 + + # Center the popup + self.x0 = (self.cb.width - self.width) // 2 + self.y0 = (self.cb.height - self.height) // 2 diff --git a/almonds/main.py b/almonds/main.py index ca13917..2923116 100644 --- a/almonds/main.py +++ b/almonds/main.py @@ -1,16 +1,16 @@ # -*- encoding: utf-8 -*- -from __future__ import print_function -from __future__ import division + + import os import sys import multiprocessing import argparse -from almonds import splash -from almonds import __version__ -from almonds import main +from .almonds import splash +from .almonds import __version__ +from .almonds import main def wrap_prolog(func, prolog): diff --git a/almonds/main.py.bak b/almonds/main.py.bak new file mode 100644 index 0000000..ca13917 --- /dev/null +++ b/almonds/main.py.bak @@ -0,0 +1,61 @@ +# -*- encoding: utf-8 -*- + +from __future__ import print_function +from __future__ import division + +import os +import sys +import multiprocessing +import argparse + +from almonds import splash +from almonds import __version__ +from almonds import main + + +def wrap_prolog(func, prolog): + def wrapped(*args, **kwargs): + print(prolog) + func(*args, **kwargs) + return wrapped + + +def launch(): + if os.name == "nt" and sys.platform != "cygwin": + __name__ = "__main__" + + parser = argparse.ArgumentParser(description="version " + __version__, prog="almonds", + formatter_class=lambda prog: + argparse.RawTextHelpFormatter(prog, max_help_position=45)) + + parser.add_argument("save", nargs="?", type=str, default=None, + help="path of a save to load") + parser.add_argument("-p", "--processes", type=int, metavar="N", + default=multiprocessing.cpu_count(), + help="number of concurrent processes") + + group = parser.add_mutually_exclusive_group() + group.add_argument("-r", "--char-ratio", type=float, + default=0.428, metavar="RATIO", + help="width to height ratio of the terminal characters") + group.add_argument("-d", "--dimensions", type=int, nargs=2, + metavar=("W", "H"), help="width and height of the terminal characters") + + parser.add_argument("-z", "--qwertz", action="store_true", default=False, + help='swap the "z" and "y" keys') + + parser.print_help = wrap_prolog(parser.print_help, "\n".join(splash)) + parser.print_usage = wrap_prolog(parser.print_usage, "") + + args = parser.parse_args() + + ratio = args.char_ratio + if args.dimensions is not None: + ratio = args.dimensions[0] / args.dimensions[1] + + pool = multiprocessing.Pool(args.processes) + main(pool, ratio, args.qwertz, args.save) + + +if __name__ == "__main__": + launch() diff --git a/almonds/mandelbrot.py b/almonds/mandelbrot.py index f0f04a8..51ec287 100644 --- a/almonds/mandelbrot.py +++ b/almonds/mandelbrot.py @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- -from __future__ import division + import cmath import sys diff --git a/almonds/mandelbrot.py.bak b/almonds/mandelbrot.py.bak new file mode 100644 index 0000000..f0f04a8 --- /dev/null +++ b/almonds/mandelbrot.py.bak @@ -0,0 +1,132 @@ +# -*- encoding: utf-8 -*- + +from __future__ import division + +import cmath +import sys + +from .utils import * + + +def mandelbrot_iterate(c, max_iterations, julia_seed=None): + """ + Returns the number of iterations before escaping the Mandelbrot fractal. + + :param c: Coordinates as a complex number + :type c: complex + :param max_iterations: Limit of how many tries are attempted. + :return: Tuple containing the last complex number in the sequence and the number of iterations. + """ + z = c + if julia_seed is not None: + c = julia_seed + for iterations in range(max_iterations): + z = z * z + c + if abs(z) > 1000: + return z, iterations + return z, max_iterations + + +def get_coords(x, y, params): + """ + Transforms the given coordinates from plane-space to Mandelbrot-space (real and imaginary). + + :param x: X coordinate on the plane. + :param y: Y coordinate on the plane. + :param params: Current application parameters. + :type params: params.Params + :return: Tuple containing the re-mapped coordinates in Mandelbrot-space. + """ + n_x = x * 2.0 / params.plane_w * params.plane_ratio - 1.0 + n_y = y * 2.0 / params.plane_h - 1.0 + mb_x = params.zoom * n_x + mb_y = params.zoom * n_y + return mb_x, mb_y + + +def mandelbrot(x, y, params): + """ + Computes the number of iterations of the given plane-space coordinates. + + :param x: X coordinate on the plane. + :param y: Y coordinate on the plane. + :param params: Current application parameters. + :type params: params.Params + :return: Discrete number of iterations. + """ + mb_x, mb_y = get_coords(x, y, params) + mb = mandelbrot_iterate(mb_x + 1j * mb_y, params.max_iterations, params.julia_seed) + + return mb[1] + + +def mandelbrot_capture(x, y, w, h, params): + """ + Computes the number of iterations of the given pixel-space coordinates, + for high-res capture purposes. + + Contrary to :func:`mandelbrot`, this function returns a continuous + number of iterations to avoid banding. + + :param x: X coordinate on the picture + :param y: Y coordinate on the picture + :param w: Width of the picture + :param h: Height of the picture + :param params: Current application parameters. + :type params: params.Params + :return: Continuous number of iterations. + """ + + # FIXME: Figure out why these corrections are necessary or how to make them perfect + # Viewport is offset compared to window when capturing without these (found empirically) + if params.plane_ratio >= 1.0: + x -= params.plane_w + else: + x += 3.0 * params.plane_w + + ratio = w / h + n_x = x * 2.0 / w * ratio - 1.0 + n_y = y * 2.0 / h - 1.0 + mb_x = params.zoom * n_x + params.mb_cx + mb_y = params.zoom * n_y + params.mb_cy + + mb = mandelbrot_iterate(mb_x + 1j * mb_y, params.max_iterations, params.julia_seed) + z, iterations = mb + + # Continuous iteration count for no banding + # https://en.wikipedia.org/wiki/Mandelbrot_set#Continuous_.28smooth.29_coloring + nu = params.max_iterations + if iterations < params.max_iterations: + nu = iterations + 2 - abs(cmath.log(cmath.log(abs(z)) / cmath.log(params.max_iterations), 2)) + + return clamp(nu, 0, params.max_iterations) + + +def update_position(params): + """ + Computes the center of the viewport's Mandelbrot-space coordinates. + + :param params: Current application parameters. + :type params: params.Params + """ + cx = params.plane_x0 + params.plane_w / 2.0 + cy = params.plane_y0 + params.plane_h / 2.0 + params.mb_cx, params.mb_cy = get_coords(cx, cy, params) + + +def zoom(params, factor): + """ + Applies a zoom on the current parameters. + + Computes the top-left plane-space coordinates from the Mandelbrot-space coordinates. + + :param params: Current application parameters. + :param factor: Zoom factor by which the zoom ratio is divided (bigger factor, more zoom) + """ + params.zoom /= factor + + n_x = params.mb_cx / params.zoom + n_y = params.mb_cy / params.zoom + + params.plane_x0 = int((n_x + 1.0) * params.plane_w / (2.0 * params.plane_ratio)) - params.plane_w // 2 + params.plane_y0 = int((n_y + 1.0) * params.plane_h / 2.0) - params.plane_h // 2 diff --git a/almonds/params.py b/almonds/params.py index dd57724..879d1ab 100644 --- a/almonds/params.py +++ b/almonds/params.py @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- -from __future__ import division + import os import sys diff --git a/almonds/params.py.bak b/almonds/params.py.bak new file mode 100644 index 0000000..dd57724 --- /dev/null +++ b/almonds/params.py.bak @@ -0,0 +1,83 @@ +# -*- encoding: utf-8 -*- + +from __future__ import division + +import os +import sys + +from .utils import is_native_windows + + +class Params(object): + """ + Class representing the current state of Almonds. + """ + def __init__(self, log, char_ratio): + """ + Initializes the parameters. + + :param log: Logger to use in the program. + :type log: logger.Logger + """ + self.log = log + self.char_ratio = char_ratio + + # Mandelbrot parameters + self.zoom = 1.0 # Current zoom level factor + self.max_iterations = 40 # Number of maximum iterations + self.mb_cx = -0.5 # Real position center + self.mb_cy = 0.0 # Imaginary position center + + # Appearance + self.palette = 0 # Color palette used + self.dither_type = 2 # Type of text characters used + self.reverse_palette = False # If true, palette is read backwards + self.adaptive_palette = False # Stretches the palette to what's currently visible + self.progress = 0 # Used for progress bars + self.palette_offset = 0 # Temporary offset for color cycling + self.crosshairs = False + self.crosshairs_coord = None + if is_native_windows(): + self.dither_type = 0 + + # Infinite plane that stores results + self.plane_x0 = None # Plane coordinate of leftmost position on the displayed screen + self.plane_y0 = None # Plane coordinate of rightmost position on the displayed screen + self.plane_w = None # Width of the currently displayed screen + self.plane_h = None # Height of the currently displayed screen + self.plane_ratio = None # Ratio of the current screen, including the char ratio + self.move_speed = 1 # Number of skipped plane cells for arrow keys movement + + # Backups for when switching to julia + self.julia = False + self.julia_seed = None + self.old_zoom = 1.0 + + def toggle_julia(self): + if self.julia: + self.zoom = self.old_zoom + self.mb_cx = self.julia_seed.real + self.mb_cy = self.julia_seed.imag + self.julia_seed = None + self.julia = False + else: + self.old_zoom = self.zoom + self.zoom = 1.0 + self.julia_seed = self.mb_cx + self.mb_cy * 1j + self.mb_cx = 0.0 + self.mb_cy = 0.0 + self.julia = True + + def resize(self, w, h): + """ + Used when resizing the plane, resets the plane ratio factor. + + :param w: New width of the visible section of the plane. + :param h: New height of the visible section of the plane. + """ + self.plane_w = w + self.plane_h = h + self.plane_ratio = self.char_ratio * w / h + + if self.crosshairs: + self.crosshairs_coord = ((w + 2) // 2, (h + 2) // 2) diff --git a/almonds/tests/test_drawing.py b/almonds/tests/test_drawing.py index b3a9c01..914300d 100644 --- a/almonds/tests/test_drawing.py +++ b/almonds/tests/test_drawing.py @@ -24,7 +24,7 @@ def test_dither_symbol(): def test_draw_box(): cb = CurseBox(15, 5) draw_box(cb, 0, 0, cb.width, cb.height, v_seps=[7]) - expected = dedent(u""" + expected = dedent(""" ┌──────┬──────┐ │ │ │ │ │ │ @@ -53,7 +53,7 @@ def test_fill(): cb = CurseBox(10, 5) draw_box(cb, 0, 0, 10, 5) fill(cb, 1, 1, 9, 3, "#") - expected = dedent(u""" + expected = dedent(""" ┌────────┐ │########│ │########│ diff --git a/almonds/tests/test_drawing.py.bak b/almonds/tests/test_drawing.py.bak new file mode 100644 index 0000000..b3a9c01 --- /dev/null +++ b/almonds/tests/test_drawing.py.bak @@ -0,0 +1,70 @@ +# -*- encoding: utf-8 -*- + +from textwrap import dedent + +from ..graphics.drawing import * +from .fake_terminal import CurseBox + + +def test_sort_palette(): + palette = [colors.white, colors.black, colors.blue, colors.cyan] + sorted_palette = sort_palette(palette) + + assert sorted_palette == [colors.black, colors.blue, colors.cyan, colors.white] + + +def test_dither_symbol(): + values = [0.00, 0.25, 0.50, 0.75, 1.00] + chars = [dither_symbol(value, 0) for value in values] + + for i, char in enumerate(chars): + assert char == symbols.dither1[i] + + +def test_draw_box(): + cb = CurseBox(15, 5) + draw_box(cb, 0, 0, cb.width, cb.height, v_seps=[7]) + expected = dedent(u""" + ┌──────┬──────┐ + │ │ │ + │ │ │ + │ │ │ + └──────┴──────┘ + """)[1:] + + assert expected == cb.get_screen()[0] + + +def test_draw_text(): + cb = CurseBox(13, 1) + draw_text(cb, 0, 0, "Hello, $World$!") + expected_chars = "Hello, World!\n" + + cn = (colors.default_fg(), colors.default_bg()) + ci = cn[::-1] + expected_colors = [[cn] * 7 + [ci] * 5 + [cn]] + result_chars, result_colors = cb.get_screen() + + assert expected_chars == result_chars + assert expected_colors == result_colors + + +def test_fill(): + cb = CurseBox(10, 5) + draw_box(cb, 0, 0, 10, 5) + fill(cb, 1, 1, 9, 3, "#") + expected = dedent(u""" + ┌────────┐ + │########│ + │########│ + │########│ + └────────┘ + """)[1:] + assert expected == cb.get_screen()[0] + + +def test_interpolate(): + c1 = (255, 0, 0) + c2 = (0, 0, 255) + c3 = interpolate(c1, c2, 0.5) + assert c3 == (127, 0, 127) diff --git a/almonds/utils.py b/almonds/utils.py index a492c02..0f99f62 100644 --- a/almonds/utils.py +++ b/almonds/utils.py @@ -83,4 +83,4 @@ def range(*args, **kwargs): import builtins return builtins.range(*args, **kwargs) else: - return xrange(*args, **kwargs) + return range(*args, **kwargs) diff --git a/almonds/utils.py.bak b/almonds/utils.py.bak new file mode 100644 index 0000000..a492c02 --- /dev/null +++ b/almonds/utils.py.bak @@ -0,0 +1,86 @@ +# -*- encoding: utf-8 -*- + +import sys +import os +import subprocess + + +def clamp(n, lower, upper): + """ + Restricts the given number to a lower and upper bound (inclusive) + + :param n: input number + :param lower: lower bound (inclusive) + :param upper: upper bound (inclusive) + :return: clamped number + """ + if lower > upper: + lower, upper = upper, lower + return max(min(upper, n), lower) + + +def screen_resolution(): + """ + Returns the current screen's resolution. + + Should be multi-platform. + + :return: A tuple containing the width and height of the screen. + """ + w = 0 + h = 0 + try: + # Windows + import ctypes + user32 = ctypes.windll.user32 + w, h = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1) + except AttributeError: + try: + # Mac OS X + import AppKit + size = AppKit.NSScreen.screens()[0].frame().size + w, h = int(size.width), int(size.height) + except ImportError: + try: + # Linux + import Xlib + import Xlib.display + display = Xlib.display.Display() + root = display.screen().root + size = root.get_geometry() + w, h = size.width, size.height + except ImportError: + w = 1920 + h = 1080 + + return w, h + + +def open_file(filename): + """ + Multi-platform way to make the OS open a file with its default application + """ + if sys.platform.startswith("darwin"): + subprocess.call(("open", filename)) + elif sys.platform == "cygwin": + subprocess.call(("cygstart", filename)) + elif os.name == "nt": + os.system("start %s" % filename) + elif os.name == "posix": + subprocess.call(("xdg-open", filename)) + + +def is_native_windows(): + return os.name == "nt" and sys.platform != "cygwin" + + +def is_python3(): + return sys.version_info[0] > 2 + + +def range(*args, **kwargs): + if is_python3(): + import builtins + return builtins.range(*args, **kwargs) + else: + return xrange(*args, **kwargs)