├── requirements.txt ├── src └── aptus │ ├── web │ ├── __init__.py │ ├── static │ │ ├── icon.png │ │ ├── style.css │ │ └── style.scss │ ├── templates │ │ ├── icon_close.svg │ │ └── mainpage.html │ └── server.py │ ├── icon16.png │ ├── icon32.png │ ├── icon48.png │ ├── splash.png │ ├── palettes │ ├── redblue.ggr │ ├── ib15.ggr │ ├── bluefly.ggr │ ├── ib18.ggr │ └── DEM_screen.ggr │ ├── __init__.py │ ├── settings.py │ ├── gui │ ├── resources.py │ ├── statspanel.py │ ├── ids.py │ ├── dictpanel.py │ ├── __init__.py │ ├── pointinfo.py │ ├── juliapanel.py │ ├── misc.py │ ├── palettespanel.py │ ├── help.py │ ├── computepanel.py │ ├── youarehere.py │ ├── mainframe.py │ └── viewpanel.py │ ├── cmdline.py │ ├── timeutil.py │ ├── progress.py │ ├── ggr.py │ ├── options.py │ └── palettes.py ├── .treerc ├── etc ├── icon.png ├── splash.xcf ├── wininst.bmp ├── crosshair.gif ├── rst_template.txt ├── icon_mask.aptus └── icon.aptus ├── README.txt ├── MANIFEST.in ├── .gitignore ├── doc ├── FringedBabies.aptus ├── DragonTails.aptus ├── GreenSeahorses.aptus ├── PaisleySpiral.aptus ├── JamesGiantPeach.aptus ├── v3.px └── index.px ├── lab ├── to_ggr.py ├── foo_test.c ├── events.py ├── f2.py ├── sugree.py ├── point.py ├── boundary.py └── test_boundary.py ├── .editorconfig ├── scripts └── timeit.py ├── test ├── test_options.py └── test_palette.py ├── Makefile ├── .github └── workflows │ └── kit.yml ├── CHANGES ├── setup.py ├── TODO.txt └── .pylintrc /requirements.txt: -------------------------------------------------------------------------------- 1 | libsass 2 | requests 3 | twine 4 | -------------------------------------------------------------------------------- /src/aptus/web/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import main 2 | -------------------------------------------------------------------------------- /.treerc: -------------------------------------------------------------------------------- 1 | [default] 2 | ignore = 3 | *.pyc 4 | build 5 | -------------------------------------------------------------------------------- /etc/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedbat/aptus/HEAD/etc/icon.png -------------------------------------------------------------------------------- /etc/splash.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedbat/aptus/HEAD/etc/splash.xcf -------------------------------------------------------------------------------- /etc/wininst.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedbat/aptus/HEAD/etc/wininst.bmp -------------------------------------------------------------------------------- /etc/crosshair.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedbat/aptus/HEAD/etc/crosshair.gif -------------------------------------------------------------------------------- /src/aptus/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedbat/aptus/HEAD/src/aptus/icon16.png -------------------------------------------------------------------------------- /src/aptus/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedbat/aptus/HEAD/src/aptus/icon32.png -------------------------------------------------------------------------------- /src/aptus/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedbat/aptus/HEAD/src/aptus/icon48.png -------------------------------------------------------------------------------- /src/aptus/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedbat/aptus/HEAD/src/aptus/splash.png -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Aptus Mandelbrot explorer and renderer! 2 | 3 | http://nedbatchelder.com/code/aptus/v3.html 4 | -------------------------------------------------------------------------------- /src/aptus/web/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedbat/aptus/HEAD/src/aptus/web/static/icon.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include scripts *.py 2 | recursive-include src *.py 3 | recursive-include src *.png 4 | recursive-include src *.html 5 | recursive-include src *.js 6 | include CHANGES 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files that can appear anywhere in the tree. 2 | *.pyc 3 | *.pyo 4 | *.pyd 5 | *.so 6 | *.bak 7 | .DS_Store 8 | 9 | # Stuff in the root. 10 | build/ 11 | dist/ 12 | Aptus.egg-info/ 13 | MANIFEST 14 | 15 | doc/*.png 16 | -------------------------------------------------------------------------------- /src/aptus/palettes/redblue.ggr: -------------------------------------------------------------------------------- 1 | GIMP Gradient 2 | Name: Foo 3 | 2 4 | 0.000000 0.248494 0.496988 0.956863 0.198877 0.198877 1.000000 0.290792 0.446460 0.972549 1.000000 0 2 5 | 0.496988 0.748494 1.000000 0.290792 0.446460 0.972549 1.000000 0.956863 0.198877 0.198877 1.000000 0 1 6 | -------------------------------------------------------------------------------- /src/aptus/web/templates/icon_close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /doc/FringedBabies.aptus: -------------------------------------------------------------------------------- 1 | {"Aptus state": 1, 2 | "center": [-1.9997740568489999,-9.009320299781e-009], 3 | "diam": [1.6485199254939999e-009,1.648519925604e-009], 4 | "iter_limit": 99999, 5 | "palette_phase": 3, 6 | "supersample": 1, 7 | "size": [500,370], 8 | "palette": [["spectrum",{"ncolors":12}],["stretch",{"steps":10,"hsl":true}]] 9 | } 10 | -------------------------------------------------------------------------------- /etc/rst_template.txt: -------------------------------------------------------------------------------- 1 | %(head_prefix)s 2 | 3 | %(head)s 4 | 5 | %(stylesheet)s 6 | 7 | %(body_prefix)s 8 | 9 | %(body_pre_docinfo)s 10 | 11 | %(docinfo)s 12 | 13 | %(body)s 14 | 15 | %(body_suffix)s 16 | -------------------------------------------------------------------------------- /doc/DragonTails.aptus: -------------------------------------------------------------------------------- 1 | {"Aptus state": 1, 2 | "center": [-0.029582575351645476,0.69733037453751001], 3 | "diam": [6.7310462592735931e-006,5.7568158796650337e-006], 4 | "iter_limit": 20000, 5 | "palette_phase": 16, 6 | "supersample": 1, 7 | "size": [638,515], 8 | "palette": [["spectrum",{"ncolors":12,"l":[50,150]}],["stretch",{"steps":25,"hsl":true}]] 9 | } 10 | -------------------------------------------------------------------------------- /doc/GreenSeahorses.aptus: -------------------------------------------------------------------------------- 1 | {"Aptus state": 1, 2 | "center": [-0.7757494796145038,0.12413178266214313], 3 | "diam": [0.011206094314809789,0.0017882065395973257], 4 | "iter_limit": 99999, 5 | "palette_phase": 0, 6 | "supersample": 1, 7 | "size": [500,370], 8 | "palette": [["spectrum",{"ncolors":2,"s":125,"h":120}],["stretch",{"steps":128,"hsl":true}]] 9 | } 10 | -------------------------------------------------------------------------------- /doc/PaisleySpiral.aptus: -------------------------------------------------------------------------------- 1 | {"Aptus state": 1, 2 | "center": [-1.6739383698999999,5.0026959188999998e-006], 3 | "diam": [6.0990112716999998e-007,6.0990112716999998e-007], 4 | "iter_limit": 70000, 5 | "palette_phase": 29, 6 | "supersample": 1, 7 | "continuous": true, 8 | "size": [500,370], 9 | "palette": [["spectrum",{"ncolors":12}],["stretch",{"steps":10,"hsl":true}]] 10 | } 11 | -------------------------------------------------------------------------------- /src/aptus/palettes/ib15.ggr: -------------------------------------------------------------------------------- 1 | GIMP Gradient 2 | Name: ib15 3 | 3 4 | 0.000000 0.175049 0.350098 0.733333 0.627451 0.803922 1.000000 0.925490 0.607843 0.443137 1.000000 0 0 5 | 0.350098 0.385010 0.419922 0.925490 0.607843 0.443137 1.000000 1.000000 0.372549 0.290196 1.000000 0 0 6 | 0.419922 0.709961 1.000000 1.000000 0.372549 0.290196 1.000000 0.572549 0.396078 0.658824 1.000000 0 0 7 | -------------------------------------------------------------------------------- /src/aptus/palettes/bluefly.ggr: -------------------------------------------------------------------------------- 1 | GIMP Gradient 2 | Name: bluefly 3 | 3 4 | 0.000000 0.125000 0.250000 0.000000 0.000000 0.000000 1.000000 0.000000 0.427451 0.576471 1.000000 0 0 5 | 0.250000 0.500000 0.750000 0.000000 0.427451 0.576471 1.000000 0.866667 0.925490 0.968627 1.000000 0 0 6 | 0.750000 0.875000 1.000000 0.866667 0.925490 0.968627 1.000000 0.000000 0.000000 0.000000 1.000000 0 0 7 | -------------------------------------------------------------------------------- /doc/JamesGiantPeach.aptus: -------------------------------------------------------------------------------- 1 | {"Aptus state": 1, 2 | "center": [-1.8605327723759248,-1.270334865601334e-005], 3 | "diam": [1.788139343261719e-007,1.788139343261719e-007], 4 | "iter_limit": 999, 5 | "palette_phase": 190, 6 | "supersample": 1, 7 | "continuous": true, 8 | "size": [500,370], 9 | "palette": [["spectrum",{"ncolors":12,"l":[50,150]}],["stretch",{"steps":25,"hsl":true}]] 10 | } 11 | -------------------------------------------------------------------------------- /src/aptus/__init__.py: -------------------------------------------------------------------------------- 1 | """ Aptus Mandelbrot set viewer and renderer. 2 | """ 3 | 4 | __version__ = '3.0.1' 5 | 6 | import os.path 7 | 8 | def data_file(fname): 9 | """ Return the path to a data file of ours. 10 | """ 11 | return os.path.join(os.path.split(__file__)[0], fname) 12 | 13 | class AptusException(Exception): 14 | """ Any Aptus-raised exception. 15 | """ 16 | pass 17 | -------------------------------------------------------------------------------- /src/aptus/palettes/ib18.ggr: -------------------------------------------------------------------------------- 1 | GIMP Gradient 2 | Name: ib18 3 | 5 4 | 0.000000 0.099976 0.199951 0.419608 0.396078 0.396078 1.000000 0.839216 0.800000 0.760784 1.000000 0 0 5 | 0.199951 0.364990 0.530029 0.839216 0.800000 0.760784 1.000000 0.509804 0.470588 0.435294 1.000000 0 0 6 | 0.530029 0.604980 0.679932 0.509804 0.470588 0.435294 1.000000 0.996078 0.996078 0.996078 1.000000 0 0 7 | 0.679932 0.709961 0.739990 0.996078 0.996078 0.996078 1.000000 0.874510 0.772549 0.682353 1.000000 0 0 8 | 0.739990 0.869995 1.000000 0.874510 0.772549 0.682353 1.000000 0.839216 0.800000 0.760784 1.000000 0 0 9 | -------------------------------------------------------------------------------- /src/aptus/palettes/DEM_screen.ggr: -------------------------------------------------------------------------------- 1 | GIMP Gradient 2 | Name: DEM_screen 3 | 5 4 | 0.000000 0.062500 0.125000 0.000000 0.517647 0.207843 1.000000 0.200000 0.800000 0.000000 1.000000 0 0 5 | 0.125000 0.187500 0.250000 0.200000 0.800000 0.000000 1.000000 0.956863 0.941176 0.443137 1.000000 0 0 6 | 0.250000 0.375000 0.500000 0.956863 0.941176 0.443137 1.000000 0.956863 0.741176 0.270588 1.000000 0 0 7 | 0.500000 0.625000 0.750000 0.956863 0.741176 0.270588 1.000000 0.600000 0.392157 0.168627 1.000000 0 0 8 | 0.750000 0.875000 1.000000 0.600000 0.392157 0.168627 1.000000 1.000000 1.000000 1.000000 1.000000 0 0 9 | -------------------------------------------------------------------------------- /lab/to_ggr.py: -------------------------------------------------------------------------------- 1 | from palettes import xaos_colors 2 | #xaos_colors = xaos_colors[:10] 3 | print "GIMP Gradient" 4 | print "Name: Xaos" 5 | print "%d" % len(xaos_colors) 6 | fmt = "%.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f %d %d" 7 | for i in range(len(xaos_colors)): 8 | i1 = (i+1) % len(xaos_colors) 9 | cl = xaos_colors[i] 10 | cr = xaos_colors[i1] 11 | sl = float(i)/len(xaos_colors) 12 | sr = float(i1)/len(xaos_colors) 13 | if sr < sl: 14 | assert sr == 0 15 | sr = 1 16 | print fmt % (sl, (sl+sr)/2, sr, cl[0]/255.0, cl[1]/255.0, cl[2]/255.0, 1, cr[0]/255.0, cr[1]/255.0, cr[2]/255.0, 1, 0, 0) 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://EditorConfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 4 10 | indent_style = space 11 | insert_final_newline = true 12 | max_line_length = 80 13 | trim_trailing_whitespace = true 14 | 15 | [*.py] 16 | max_line_length = 100 17 | 18 | [*.c] 19 | max_line_length = 100 20 | 21 | [*.h] 22 | max_line_length = 100 23 | 24 | [*.yml] 25 | indent_size = 2 26 | 27 | [*.rst] 28 | max_line_length = 79 29 | 30 | [Makefile] 31 | indent_style = tab 32 | indent_size = 8 33 | 34 | [*,cover] 35 | trim_trailing_whitespace = false 36 | 37 | [*.diff] 38 | trim_trailing_whitespace = false 39 | 40 | [.git/*] 41 | trim_trailing_whitespace = false 42 | -------------------------------------------------------------------------------- /src/aptus/settings.py: -------------------------------------------------------------------------------- 1 | """ Defaults and settings for Aptus. 2 | """ 3 | 4 | # For now, simple constants. Someday, maybe preferences. 5 | 6 | mandelbrot_center = -0.6, 0.0 7 | mandelbrot_diam = 3.0 8 | explorer_size = 600, 600 9 | 10 | julia_center = 0.0, 0.0 11 | julia_diam = 3.0 12 | 13 | def center(mode='mandelbrot'): 14 | """ What's the default center for this computation mode? 15 | """ 16 | if mode == 'mandelbrot': 17 | c = mandelbrot_center 18 | elif mode == 'julia': 19 | c = julia_center 20 | else: 21 | raise Exception("Unknown mode: %r" % mode) 22 | return c 23 | 24 | def diam(mode='mandelbrot'): 25 | """ What's the default diameter for this computation mode? 26 | """ 27 | if mode == 'mandelbrot': 28 | d = mandelbrot_diam 29 | elif mode == 'julia': 30 | d = julia_diam 31 | else: 32 | raise Exception("Unknown mode: %r" % mode) 33 | return d, d 34 | -------------------------------------------------------------------------------- /etc/icon_mask.aptus: -------------------------------------------------------------------------------- 1 | {"Aptus state": 1, 2 | "center": [-0.6423821844477835, 0.0], 3 | "diam": [2.2539444027047328, 2.2539444027047328], 4 | "angle": 90, 5 | "iter_limit": 999, 6 | "size": [640, 640], 7 | "palette": [["rgb_colors", {"colors": [ 8 | [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], 9 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 10 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 11 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 12 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 13 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 14 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255]]}], 15 | ["rgb_incolor", {"color": [255, 255, 255]}] 16 | ], 17 | "palette_phase": 0, 18 | "supersample": 1 19 | } 20 | -------------------------------------------------------------------------------- /src/aptus/gui/resources.py: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------- 2 | # This file was generated by /Python25/Scripts/img2py 3 | # 4 | from wx import Image, BitmapFromImage 5 | import io 6 | import zlib 7 | 8 | 9 | def getCrosshairData(): 10 | return zlib.decompress( 11 | b"x\xda\xeb\x0c\xf0s\xe7\xe5\x92\xe2b``\xe0\xf5\xf4p\t\x02\xd2\x02 \xcc\xc1\ 12 | \x06$\xe5?\xffO\x04R,\xc5N\x9e!\x1c@P\xc3\x91\xd2\x01\xe4g{\xba8\x86X\xf4\ 13 | \xde\x9dt\x90\xeb\x80\x01\x87s\xed\xad\xff\xff\x1f\xeb\xed\xde\xb2xC\xd8)=\ 14 | \xe9\xd3\x9e\x01\xc9\x1e,\xfc\xd1\x8e\xee\xa2r:\xecg\xcelN\xf5\\\xb6\xf8\x95\ 15 | \xe0A\xbd:\x07\x06\xb9\xe0\xc5n\xdf7\xd7\xbd\xcc\xbb\xbf\xc4u\xe7\xabHy\x019\ 16 | \x11\x1b\x864\xf1c\x0f\xad\x1d\x83\x0f\xbc\x9a\xb8\xcb\xe8\xc0\xa6\xf4*\xfbV\ 17 | ~\xe1-%A\xccg'\x89\x94\x1525.\xd2\ngpRI\x03Z\xcb\xe0\xe9\xea\xe7\xb2\xce)\ 18 | \xa1\t\x00)E<\xd9" ) 19 | 20 | def getCrosshairBitmap(): 21 | return BitmapFromImage(getCrosshairImage()) 22 | 23 | def getCrosshairImage(): 24 | stream = io.BytesIO(getCrosshairData()) 25 | return Image(stream) 26 | -------------------------------------------------------------------------------- /src/aptus/cmdline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | from PIL import Image 6 | 7 | from aptus.compute import AptusCompute 8 | from aptus.progress import IntervalProgressReporter, ConsoleProgressReporter 9 | from aptus.options import AptusOptions 10 | 11 | 12 | class AptusCmdApp(): 13 | def main(self, args): 14 | """ The main for the Aptus command-line tool. 15 | """ 16 | compute = AptusCompute() 17 | opts = AptusOptions(compute) 18 | opts.read_args(args) 19 | compute.create_mandel() 20 | 21 | compute.progress = IntervalProgressReporter(60, ConsoleProgressReporter()) 22 | compute.compute_pixels() 23 | pix = compute.color_mandel() 24 | im = Image.fromarray(pix) 25 | if compute.supersample > 1: 26 | print("Resampling image...") 27 | im = im.resize(compute.size, Image.LANCZOS) 28 | compute.write_image(im, compute.outfile) 29 | 30 | 31 | def main(argv=None): 32 | if argv is None: 33 | argv = sys.argv[1:] 34 | AptusCmdApp().main(argv) 35 | 36 | 37 | if __name__ == '__main__': 38 | main() 39 | -------------------------------------------------------------------------------- /etc/icon.aptus: -------------------------------------------------------------------------------- 1 | {"Aptus state": 1, 2 | "center": [-0.6423821844477835, 0.0], 3 | "diam": [2.2539444027047328, 2.2539444027047328], 4 | "angle": 90, 5 | "iter_limit": 999, 6 | "size": [640, 640], 7 | "palette": [["rgb_colors", {"colors": [ 8 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 9 | [70, 185, 70], [70, 185, 70], [70, 185, 70], [70, 185, 70], [70, 185, 70], 10 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 11 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 12 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 13 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 14 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 15 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 16 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 17 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], 18 | [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255]]}]], 19 | "palette_phase": 0, 20 | "supersample": 1 21 | } 22 | -------------------------------------------------------------------------------- /lab/foo_test.c: -------------------------------------------------------------------------------- 1 | 2 | static int 3 | foo_count(int xi, int yi) 4 | { 5 | aptfloat f = 1.0; 6 | 7 | int i; 8 | for (i = 0; i < max_iter; i++) { 9 | f *= 1.0000001; 10 | } 11 | 12 | return 27; 13 | } 14 | 15 | static PyObject * 16 | foo_point(PyObject *self, PyObject *args) 17 | { 18 | int xi, yi; 19 | 20 | if (!PyArg_ParseTuple(args, "ii", &xi, &yi)) { 21 | return NULL; 22 | } 23 | 24 | int count = foo_count(xi, yi); 25 | 26 | return Py_BuildValue("i", count); 27 | } 28 | 29 | static PyObject * 30 | foo_array(PyObject *self, PyObject *args) 31 | { 32 | PyArrayObject *arr; 33 | 34 | if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &arr)) { 35 | return NULL; 36 | } 37 | 38 | if (arr == NULL) { 39 | return NULL; 40 | } 41 | 42 | int w = PyArray_DIM(arr, 1); 43 | int h = PyArray_DIM(arr, 0); 44 | int xi, yi; 45 | for (yi = 0; yi < h; yi++) { 46 | for (xi = 0; xi < w; xi++) { 47 | *(npy_uint32 *)PyArray_GETPTR2(arr, yi, xi) = foo_count(xi, -yi); 48 | } 49 | } 50 | 51 | return Py_BuildValue(""); 52 | } 53 | 54 | static PyMethodDef 55 | mandext_methods[] = { 56 | {"foo_point", foo_point, METH_VARARGS, ""}, 57 | {"foo_array", foo_array, METH_VARARGS, ""}, 58 | {NULL, NULL} 59 | }; 60 | 61 | -------------------------------------------------------------------------------- /src/aptus/gui/statspanel.py: -------------------------------------------------------------------------------- 1 | """ A panel to display computation statistics. 2 | """ 3 | 4 | import wx 5 | 6 | from aptus.compute import ComputeStats 7 | from aptus.gui.dictpanel import DictPanel 8 | from aptus.gui.ids import * 9 | from aptus.gui.misc import AptusToolFrame, ListeningWindowMixin 10 | 11 | 12 | class StatsPanel(DictPanel, ListeningWindowMixin): 13 | """ A panel displaying the statistics from a view window. It listens for 14 | recomputations, and updates automatically. 15 | """ 16 | 17 | def __init__(self, parent, viewwin): 18 | """ Create a StatsPanel, with `parent` as its parent, and `viewwin` as 19 | the window to track. 20 | """ 21 | DictPanel.__init__(self, parent, ComputeStats.statmap) 22 | ListeningWindowMixin.__init__(self) 23 | 24 | self.viewwin = viewwin 25 | self.register_listener(self.on_recomputed, EVT_APTUS_RECOMPUTED, self.viewwin) 26 | 27 | # Need to call on_recomputed after the window appears, so that the widths of 28 | # the text controls can be set properly. Else, it all appears left-aligned. 29 | wx.CallAfter(self.on_recomputed) 30 | 31 | def on_recomputed(self, event_unused=None): 32 | stats = self.viewwin.get_stats() 33 | self.update(stats) 34 | 35 | 36 | class StatsFrame(AptusToolFrame): 37 | def __init__(self, mainframe, viewwin): 38 | AptusToolFrame.__init__(self, mainframe, title='Statistics', size=(180,250)) 39 | self.panel = StatsPanel(self, viewwin) 40 | -------------------------------------------------------------------------------- /scripts/timeit.py: -------------------------------------------------------------------------------- 1 | from aptus.compute import AptusCompute 2 | from aptus.progress import NullProgressReporter 3 | from aptus import settings 4 | 5 | import sys 6 | import time 7 | 8 | def timeit(args): 9 | compute = AptusCompute() 10 | compute.progress = NullProgressReporter() 11 | compute.size = 5000, 5000 12 | 13 | grandtotal = 0 14 | 15 | if not args: 16 | case = 'a' 17 | else: 18 | case = args[0] 19 | 20 | if case == 'a': 21 | compute.center = settings.mandelbrot_center 22 | compute.diam = settings.mandelbrot_diam, settings.mandelbrot_diam 23 | nruns = 100 24 | elif case == 'b': 25 | compute.center = -1.8605327670201655, -1.2705648690517021e-005 26 | compute.diam = 2.92062690996144e-010, 2.92062690996144e-010 27 | compute.iter_limit = 99999 28 | nruns = 20 29 | elif case == 'c': 30 | compute.center = -1.0030917862909408, -0.28088298837940889 31 | compute.diam = 1.3986199517069311e-008, 1.2034636731605979e-008 32 | compute.iter_limit = 99999 33 | nruns = 5 34 | else: 35 | print("huh?") 36 | return 37 | 38 | for i in range(nruns): 39 | compute.clear_results() 40 | compute.create_mandel() 41 | start = time.time() 42 | compute.compute_pixels() 43 | total = time.time() - start 44 | print("%.4f" % total) 45 | grandtotal += total 46 | 47 | print("Average %.5f over %d runs" % (grandtotal/nruns, nruns)) 48 | 49 | 50 | if __name__ == '__main__': 51 | timeit(sys.argv[1:]) 52 | -------------------------------------------------------------------------------- /src/aptus/gui/ids.py: -------------------------------------------------------------------------------- 1 | """ Ids and events for Aptus. 2 | """ 3 | 4 | import wx 5 | import wx.lib.newevent 6 | 7 | ## Custom events 8 | 9 | # The coloring of a view window changed. 10 | AptusColoringChangedEvent, EVT_APTUS_COLORING_CHANGED = wx.lib.newevent.NewEvent() 11 | 12 | # The computation parameters of a view window changed: iterlimit, continuous, etc. 13 | AptusComputationChangedEvent, EVT_APTUS_COMPUTATION_CHANGED = wx.lib.newevent.NewEvent() 14 | 15 | # The geometry of a view window changed: position, angle, size. 16 | AptusGeometryChangedEvent, EVT_APTUS_GEOMETRY_CHANGED = wx.lib.newevent.NewEvent() 17 | 18 | # A view window just finished recomputing. 19 | AptusRecomputedEvent, EVT_APTUS_RECOMPUTED = wx.lib.newevent.NewEvent() 20 | 21 | # User indicated a new point in a view window, point= is client point coords. 22 | AptusIndicatePointEvent, EVT_APTUS_INDICATEPOINT = wx.lib.newevent.NewEvent() 23 | 24 | ## Command ids 25 | 26 | id_set_angle = wx.NewId() 27 | id_save = wx.NewId() 28 | id_set_iter_limit = wx.NewId() 29 | id_toggle_continuous = wx.NewId() 30 | id_toggle_julia = wx.NewId() 31 | id_jump = wx.NewId() 32 | id_redraw = wx.NewId() 33 | id_change_palette = wx.NewId() # data: palette index delta 34 | id_set_palette = wx.NewId() # data: palette index 35 | id_cycle_palette = wx.NewId() 36 | id_scale_palette = wx.NewId() 37 | id_adjust_palette = wx.NewId() 38 | id_reset_palette = wx.NewId() 39 | id_help = wx.NewId() 40 | id_fullscreen = wx.NewId() 41 | id_new = wx.NewId() 42 | id_show_youarehere = wx.NewId() 43 | id_show_palettes = wx.NewId() 44 | id_show_stats = wx.NewId() 45 | id_show_pointinfo = wx.NewId() 46 | id_show_julia = wx.NewId() 47 | id_window_size = wx.NewId() 48 | id_open = wx.NewId() 49 | -------------------------------------------------------------------------------- /test/test_options.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from aptus.options import * 4 | 5 | class OptionsTestTarget: 6 | def __init__(self): 7 | self.angle = 0 8 | self.continuous = False 9 | self.iter_limit = 10 10 | self.outfile = None 11 | self.size = [100, 100] 12 | 13 | class OptionsTest(unittest.TestCase): 14 | 15 | def try_read_args(self, cmdline): 16 | argv = cmdline.split() 17 | target = OptionsTestTarget() 18 | AptusOptions(target).read_args(argv) 19 | return target 20 | 21 | def testNoArgs(self): 22 | target = self.try_read_args("") 23 | self.assertEqual(target.angle, 0) 24 | self.assertEqual(target.continuous, False) 25 | 26 | def testSize(self): 27 | target = self.try_read_args("-s 300x200") 28 | self.assertEqual(target.size, [300, 200]) 29 | target = self.try_read_args("--size 300x200") 30 | self.assertEqual(target.size, [300, 200]) 31 | target = self.try_read_args("--size=300x200") 32 | self.assertEqual(target.size, [300, 200]) 33 | target = self.try_read_args("-s 300,200") 34 | self.assertEqual(target.size, [300, 200]) 35 | target = self.try_read_args("--size 300,200") 36 | self.assertEqual(target.size, [300, 200]) 37 | target = self.try_read_args("--size=300,200") 38 | self.assertEqual(target.size, [300, 200]) 39 | 40 | def testMisc(self): 41 | target = self.try_read_args("-c") 42 | self.assertEqual(target.continuous, True) 43 | 44 | def testFloatPair(self): 45 | target = self.try_read_args("--center=1.5x2.5") 46 | self.assertEqual(target.center, [1.5, 2.5]) 47 | target = self.try_read_args("--center=1.275") 48 | self.assertEqual(target.center, [1.275, 1.275]) 49 | -------------------------------------------------------------------------------- /src/aptus/gui/dictpanel.py: -------------------------------------------------------------------------------- 1 | """ A panel for displaying information from a dictionary. 2 | """ 3 | 4 | import wx 5 | 6 | # Set the locale to the user's default. 7 | import locale 8 | locale.setlocale(locale.LC_ALL, "") 9 | 10 | 11 | class DictPanel(wx.Panel): 12 | """ A panel displaying the contents of a dictionary. 13 | """ 14 | def __init__(self, parent, keymap): 15 | wx.Panel.__init__(self, parent) 16 | self.keymap = keymap 17 | self.keywins = [] 18 | 19 | grid = wx.FlexGridSizer(cols=2, vgap=1, hgap=3) 20 | for keyd in self.keymap: 21 | label = wx.StaticText(self, -1, keyd['label'] + ':') 22 | value = wx.StaticText(self, -1, style=wx.ALIGN_RIGHT) 23 | grid.Add(label) 24 | grid.Add(value) 25 | self.keywins.append((keyd, value)) 26 | 27 | sizer = wx.BoxSizer() 28 | sizer.Add(grid, flag=wx.TOP|wx.RIGHT|wx.BOTTOM|wx.LEFT, border=3) 29 | self.SetSizer(sizer) 30 | sizer.Fit(self) 31 | 32 | def update(self, dval): 33 | """ Update the values in the panel, from the dictionary `dval`. 34 | """ 35 | maxw = 50 36 | for keyd, valwin in self.keywins: 37 | val = dval[keyd['key']] 38 | if isinstance(val, int): 39 | s = locale.format(keyd.get('fmt', "%d"), val, grouping=True) 40 | elif isinstance(val, float): 41 | s = locale.format(keyd.get('fmt', "%.10e"), val, grouping=True) 42 | elif val is None: 43 | s = u"\u2014" # emdash 44 | else: 45 | s = str(val) 46 | valwin.SetLabel(s) 47 | w = valwin.GetSize()[0] 48 | maxw = max(maxw, w) 49 | 50 | for _, valwin in self.keywins: 51 | valwin.SetSize((maxw, -1)) 52 | valwin.SetMinSize((maxw, -1)) 53 | -------------------------------------------------------------------------------- /test/test_palette.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | import math 3 | 4 | from aptus.palettes import Palette 5 | 6 | def test_unstretched(): 7 | pal = Palette().spectrum(12) 8 | assert pal.colors == [ 9 | [79, 21, 21], 10 | [232, 200, 168], 11 | [79, 79, 21], 12 | [200, 232, 168], 13 | [21, 79, 21], 14 | [168, 232, 200], 15 | [21, 79, 79], 16 | [168, 200, 232], 17 | [21, 21, 79], 18 | [200, 168, 232], 19 | [79, 21, 79], 20 | [232, 168, 200], 21 | ] 22 | 23 | def test_stretched(): 24 | pal = Palette().spectrum(12).stretch(3, hsl=True) 25 | assert pal.colors == [ 26 | [79, 21, 21], 27 | [159, 61, 41], 28 | [212, 129, 88], 29 | [232, 200, 168], 30 | [212, 171, 88], 31 | [159, 139, 41], 32 | [79, 79, 21], 33 | [139, 159, 41], 34 | [171, 212, 88], 35 | [200, 232, 168], 36 | [129, 212, 88], 37 | [61, 159, 41], 38 | [21, 79, 21], 39 | [41, 159, 61], 40 | [88, 212, 129], 41 | [168, 232, 200], 42 | [88, 212, 171], 43 | [41, 159, 139], 44 | [21, 79, 79], 45 | [41, 139, 159], 46 | [88, 171, 212], 47 | [168, 200, 232], 48 | [88, 129, 212], 49 | [41, 61, 159], 50 | [21, 21, 79], 51 | [61, 41, 159], 52 | [129, 88, 212], 53 | [200, 168, 232], 54 | [171, 88, 212], 55 | [139, 41, 159], 56 | [79, 21, 79], 57 | [159, 41, 139], 58 | [212, 88, 171], 59 | [232, 168, 200], 60 | [212, 88, 129], 61 | [159, 41, 61], 62 | ] 63 | 64 | def assert_is_hue(hue, r, g, b): 65 | h, l, s = colorsys.rgb_to_hls(r/255, g/255, b/255) 66 | # black or white are ok. 67 | assert math.isclose(h, hue/360.0) or l in [0, 1] 68 | 69 | def test_one_hue(): 70 | pal = Palette().spectrum(12, h=245, l=(0, 255)).stretch(5, hsl=True) 71 | for rgb in pal.colors: 72 | assert_is_hue(245, *rgb) 73 | -------------------------------------------------------------------------------- /src/aptus/gui/__init__.py: -------------------------------------------------------------------------------- 1 | """ Aptus GUI 2 | http://nedbatchelder.com/code/aptus 3 | """ 4 | 5 | import sys 6 | 7 | from aptus import data_file 8 | from aptus.gui.mainframe import AptusMainFrame 9 | 10 | # Import third-party packages. 11 | import wx 12 | import wx.adv 13 | 14 | class AptusGuiApp(wx.App): 15 | def __init__(self, args): 16 | self.args = args 17 | wx.App.__init__(self) 18 | 19 | def OnInit(self): 20 | frame = self.new_window(self.args) 21 | SplashScreen(frame).Show() 22 | return True 23 | 24 | def new_window(self, *args, **kwargs): 25 | frame = AptusMainFrame(*args, **kwargs) 26 | frame.Show() 27 | return frame 28 | 29 | 30 | class SplashScreen(wx.adv.SplashScreen): 31 | """ A nice splash screen. 32 | """ 33 | def __init__(self, parent=None): 34 | bitmap = wx.Image(name=data_file("splash.png")).ConvertToBitmap() 35 | splash_style = wx.adv.SPLASH_TIMEOUT | wx.adv.SPLASH_NO_CENTRE 36 | style = wx.FRAME_NO_TASKBAR | wx.STAY_ON_TOP | wx.NO_BORDER 37 | wx.adv.SplashScreen.__init__(self, bitmap, splash_style, 2000, parent, style=style) 38 | self.Move(parent.ClientToScreen((0, 0)) + (50, 50)) 39 | self.Bind(wx.EVT_CLOSE, self.on_exit) 40 | 41 | def on_exit(self, evt_unused): 42 | self.alpha = 255 43 | self.timer = wx.Timer(self) 44 | self.Bind(wx.EVT_TIMER, self.fade_some) 45 | self.timer.Start(25) 46 | 47 | # Don't actually destroy the window or skip the event, so the timer can 48 | # run, and fade the window out.. 49 | 50 | def fade_some(self, evt_unused): 51 | self.alpha -= 16 52 | if self.alpha <= 0: 53 | self.timer.Stop() 54 | del self.timer 55 | self.Destroy() 56 | else: 57 | self.SetTransparent(self.alpha) 58 | 59 | 60 | def main(argv=None): 61 | """ The main for the Aptus GUI. 62 | """ 63 | if argv is None: 64 | argv = sys.argv[1:] 65 | AptusGuiApp(argv).MainLoop() 66 | -------------------------------------------------------------------------------- /lab/events.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | class MainFrame(wx.Frame): 4 | def __init__(self, parent, ID, title): 5 | wx.Frame.__init__(self, parent, ID, title, 6 | wx.DefaultPosition, wx.Size(200, 100)) 7 | 8 | Panel = wx.Panel(self, -1) 9 | #TopSizer = wx.BoxSizer(wx.VERTICAL) 10 | #Panel.SetSizer(TopSizer) 11 | 12 | #Text = wx.TextCtrl(Panel, -1, "Type text here") 13 | #TopSizer.Add(Text, 1, wx.EXPAND) 14 | 15 | #Text.Bind(wx.EVT_KEY_DOWN, self.OnKeyText) 16 | Panel.Bind(wx.EVT_KEY_DOWN, self.OnKeyPanel) 17 | Panel.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) 18 | #self.Bind(wx.EVT_KEY_DOWN, self.OnKeyFrame) 19 | Panel.SetFocus() 20 | 21 | def OnKeyText(self, event): 22 | print "OnKeyText" 23 | print "\tShould Propagate %i" % event.ShouldPropagate() 24 | Level = event.StopPropagation() 25 | print "\tPropagate level %i" % Level 26 | # Try: event.ResumePropagation(x), x=1,2,3,... 27 | event.ResumePropagation(Level) 28 | event.Skip() 29 | 30 | def OnLeftDown(self, event): 31 | print "OnLeftDown" 32 | event.GetEventObject().SetFocus() 33 | 34 | def OnKeyPanel(self, event): 35 | print "OnKeyPanel" 36 | print "\tShould Propagate %i" % event.ShouldPropagate() 37 | Level = event.StopPropagation() 38 | print "\tPropagate level %i" % Level 39 | event.ResumePropagation(Level) 40 | event.Skip() 41 | 42 | def OnKeyFrame(self, event): 43 | print "OnKeyFrame" 44 | print "\tShould Propagate %i" % event.ShouldPropagate() 45 | Level = event.StopPropagation() 46 | print "\tPropagate level %i" % Level 47 | event.ResumePropagation(Level) 48 | event.Skip() 49 | 50 | class MyApp(wx.App): 51 | def OnInit(self): 52 | Frame = MainFrame(None, -1, "Event Propagation Demo") 53 | Frame.Show(True) 54 | #self.SetTopWindow(Frame) 55 | return True 56 | 57 | if __name__ == '__main__': 58 | App = MyApp(0) 59 | App.MainLoop() 60 | 61 | -------------------------------------------------------------------------------- /src/aptus/timeutil.py: -------------------------------------------------------------------------------- 1 | """ Time utilities for Aptus. 2 | """ 3 | 4 | import time 5 | 6 | def duration(s): 7 | """ Make a nice string representation of a number of seconds `s`. 8 | """ 9 | m, s = divmod(s, 60) 10 | h, m = divmod(m, 60) 11 | d, h = divmod(h, 24) 12 | w, d = divmod(d, 7) 13 | dur = [] 14 | if w: 15 | dur.append("%dw" % w) 16 | if d: 17 | dur.append("%dd" % d) 18 | if h: 19 | dur.append("%dh" % h) 20 | if m: 21 | dur.append("%dm" % m) 22 | if s: 23 | if int(s) == s: 24 | dur.append("%ds" % s) 25 | else: 26 | dur.append("%.2fs" % s) 27 | return " ".join(dur) or "0s" 28 | 29 | def future(secs): 30 | """ Make a nice string representation of a point in time in the future, 31 | `secs` seconds from now. 32 | """ 33 | now = time.time() 34 | nowyr, nowmon, nowday, _, _, _, _, _, _ = time.localtime(now) 35 | thenyr, thenmon, thenday, _, _, _, _, _, _ = parts = time.localtime(now + secs) 36 | 37 | fmt = " " # An initial space to make the leading-zero thing work right. 38 | if (nowyr, nowmon, nowday) != (thenyr, thenmon, thenday): 39 | # A different day: decide how to describe the other day. 40 | fmt += "%a" 41 | if secs > 6*24*60*60: 42 | # More than a week away: use a real date. 43 | fmt += ", %d %b" 44 | if nowyr != thenyr: 45 | fmt += " %Y" 46 | fmt += " " 47 | # Always show the time 48 | fmt += "%I:%M:%S%p" 49 | text = time.strftime(fmt, parts) 50 | text = text.replace(" 0", " ") # Trim the leading zeros. 51 | return text[1:] # Trim the initial space. 52 | 53 | def test_it(): 54 | import sys 55 | 56 | if len(sys.argv) > 1: 57 | s = eval(sys.argv[1]) 58 | print("%d sec from now is %s" % (s, future(s))) 59 | else: 60 | for p in range(3,8): 61 | for m in [1,2,5]: 62 | s = m*10**p 63 | print("%10d sec from now is %s" % (s, future(s))) 64 | 65 | if __name__ == '__main__': 66 | test_it() 67 | -------------------------------------------------------------------------------- /lab/f2.py: -------------------------------------------------------------------------------- 1 | # An experiment in using pairs of floats to get better precision. 2 | 3 | class f2: 4 | """ Two floats, to get split precision. 5 | """ 6 | def __init__(self, a, b): 7 | self.a = float(a) + float(b) 8 | self.b = float(b) - (self.a - float(a)) 9 | 10 | def __repr__(self): 11 | return "<%r+%r>" % (self.a, self.b) 12 | 13 | def __mul__(self, o): 14 | if isinstance(o, f2): 15 | return f2(self.a*o.a, self.a*o.b + self.b*o.a + self.b*o.b) 16 | else: 17 | return f2(self.a * o, self.b * o) 18 | 19 | def __add__(self, o): 20 | if isinstance(o, f2): 21 | return f2(self.a+o.a, self.b+o.b) 22 | else: 23 | return f2(self.a + o, self.b) 24 | 25 | def __sub__(self, o): 26 | if isinstance(o, f2): 27 | return f2(self.a-o.a, self.b-o.b) 28 | else: 29 | return f2(self.a - o, self.b) 30 | 31 | def __float__(self): 32 | return self.a + self.b 33 | 34 | class c2: 35 | """ A simple complex number. 36 | """ 37 | def __init__(self, r, i): 38 | self.r, self.i = r, i 39 | 40 | def __repr__(self): 41 | return "(%r + %ri)" % (self.r, self.i) 42 | 43 | def __mul__(self, o): 44 | return c2(self.r*o.r - self.i*o.i, self.r*o.i + self.i*o.r) 45 | 46 | def __add__(self, o): 47 | return c2(self.r+o.r, self.i+o.i) 48 | 49 | def __sub__(self, o): 50 | return c2(self.r-o.r, self.i-o.i) 51 | 52 | import sys 53 | 54 | if 0: 55 | f = f2(.345678, 1.1e-20) 56 | g = float(f) 57 | p = .25 58 | 59 | for i in range(100000): 60 | print f, g 61 | f = f*f+p 62 | g = g*g+p 63 | 64 | if __name__ == '__main__': 65 | dx = 5.5951715923569399e-018 66 | x = -0.70654266100607843 67 | y = -0.36491281470843084 68 | 69 | for xi in range(100): 70 | p = c2(f2(x,xi*dx), f2(y,0)) 71 | z = c2(f2(0,0),f2(0,0)) 72 | 73 | for i in range(10000): 74 | z = z*z+p 75 | if float(z.r) > 2.0: 76 | print p, i 77 | break 78 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for utility work on Aptus 2 | 3 | install: build 4 | python setup.py install 5 | 6 | build: 7 | python setup.py build 8 | 9 | clean: 10 | -rm -rf build dist 11 | -rm -rf *.egg-info */*.egg-info */*/*.egg-info 12 | -rm -f MANIFEST 13 | -rm -f doc/*.png 14 | -rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ 15 | -rm -f *.pyc */*.pyc */*/*.pyc */*/*/*.pyc 16 | -rm -f *.pyo */*.pyo */*/*.pyo */*/*/*.pyo 17 | -rm -f *.bak */*.bak */*/*.bak */*/*/*.bak 18 | -rm -f *.so */*.so */*/*.so */*/*/*.so 19 | 20 | icon: 21 | aptuscmd --size=64x64 --super=5 --output /tmp/aptus.png etc/icon.aptus 22 | aptuscmd --size=64x64 --super=5 --output /tmp/aptus_mask.png etc/icon_mask.aptus 23 | convert /tmp/aptus.png /tmp/aptus_mask.png -compose copy-opacity -composite src/aptus/web/static/icon.png 24 | 25 | asm: 26 | gcc.exe -mno-cygwin -mdll -O -Wall -Ic:\\Python25\\lib\\site-packages\\numpy\\core\\include -Ic:\\Python25\\include -Ic:\\Python25\\PC -c ext/engine.c -O3 -g -Wa,-alh > engine.lst 27 | 28 | WEBHOME = ~/web/stellated/pages/code/aptus 29 | 30 | %.png: %.aptus 31 | aptuscmd $< --super=3 -o $*.png -s 1000x740 32 | aptuscmd $< --super=3 -o $*_med.png -s 500x370 33 | aptuscmd $< --super=5 -o $*_thumb.png -s 250x185 34 | 35 | SAMPLE_PNGS := $(patsubst %.aptus,%.png,$(wildcard doc/*.aptus)) 36 | 37 | samples: $(SAMPLE_PNGS) build 38 | 39 | publish_samples: samples 40 | cp -v doc/*.png $(WEBHOME) 41 | 42 | publish_doc: 43 | cp -v doc/*.px $(WEBHOME) 44 | 45 | publish: publish_doc publish_samples 46 | 47 | DOWNLOAD_PY = https://raw.githubusercontent.com/nedbat/coveragepy/master/ci/download_gha_artifacts.py 48 | download_kits: 49 | wget -qO - $(DOWNLOAD_PY) | python - nedbat/aptus 50 | python -m twine check dist/* 51 | 52 | kit_upload: ## Upload the built distributions to PyPI. 53 | twine upload --verbose dist/* 54 | 55 | test_upload: ## Upload the distrubutions to PyPI's testing server. 56 | twine upload --verbose --repository testpypi dist/* 57 | 58 | SCSS = src/aptus/web/static/style.scss 59 | CSS = src/aptus/web/static/style.css 60 | 61 | sass: 62 | pysassc --style=compact $(SCSS) $(CSS) 63 | 64 | livesass: 65 | echo src/aptus/web/static/style.scss | entr -n pysassc --style=compact $(SCSS) $(CSS) 66 | -------------------------------------------------------------------------------- /src/aptus/gui/pointinfo.py: -------------------------------------------------------------------------------- 1 | """ A panel to display information about the pointed-to point in the main 2 | window. 3 | """ 4 | 5 | import wx 6 | 7 | from aptus.gui.dictpanel import DictPanel 8 | from aptus.gui.ids import * 9 | from aptus.gui.misc import AptusToolFrame, ListeningWindowMixin 10 | 11 | 12 | class PointInfoPanel(DictPanel, ListeningWindowMixin): 13 | """ A panel displaying information about the current point in the main window. 14 | """ 15 | 16 | infomap = [ 17 | { 'label': 'x', 'key': 'x', }, 18 | { 'label': 'y', 'key': 'y', }, 19 | { 'label': 'r', 'key': 'r', }, 20 | { 'label': 'i', 'key': 'i', }, 21 | { 'label': 'count', 'key': 'count', 'fmt': '%.2f'}, 22 | { 'label': 'color', 'key': 'color', }, 23 | ] 24 | 25 | def __init__(self, parent, viewwin): 26 | """ Create a PointInfoPanel, with `parent` as its parent, and `viewwin` as 27 | the window to track. 28 | """ 29 | DictPanel.__init__(self, parent, self.infomap) 30 | ListeningWindowMixin.__init__(self) 31 | 32 | self.viewwin = viewwin 33 | 34 | self.register_listener(self.update_info, EVT_APTUS_RECOMPUTED, self.viewwin) 35 | self.register_listener(self.update_info, EVT_APTUS_INDICATEPOINT, self.viewwin) 36 | 37 | # Need to call update_info after the window appears, so that the widths of 38 | # the text controls can be set properly. Else, it all appears left-aligned. 39 | wx.CallAfter(self.update_info) 40 | 41 | def update_info(self, event=None): 42 | # Different events will trigger this, be flexible about how to get the 43 | # mouse position. 44 | if event and hasattr(event, 'point'): 45 | pt = event.point 46 | else: 47 | pt = self.viewwin.ScreenToClient(wx.GetMousePosition()) 48 | info = self.viewwin.get_point_info(pt) 49 | if info: 50 | self.update(info) 51 | 52 | # Need to let the main window handle the event too. 53 | if event: 54 | event.Skip() 55 | 56 | 57 | class PointInfoFrame(AptusToolFrame): 58 | def __init__(self, mainframe, viewwin): 59 | AptusToolFrame.__init__(self, mainframe, title='Point info', size=(180,180)) 60 | self.panel = PointInfoPanel(self, viewwin) 61 | -------------------------------------------------------------------------------- /.github/workflows/kit.yml: -------------------------------------------------------------------------------- 1 | # Based on: 2 | # https://github.com/joerick/cibuildwheel/blob/master/examples/github-deploy.yml 3 | 4 | name: "Kits" 5 | 6 | on: 7 | push: 8 | branches: 9 | # Don't build kits all the time, but do if the branch is about kits. 10 | - "**/*kit*" 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: "${{ github.workflow }}-${{ github.ref }}" 18 | cancel-in-progress: true 19 | 20 | defaults: 21 | run: 22 | shell: bash 23 | 24 | jobs: 25 | wheels: 26 | name: "Build wheels on ${{ matrix.os }}" 27 | runs-on: ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | os: 31 | - ubuntu-latest 32 | - windows-latest 33 | - macos-latest 34 | fail-fast: false 35 | 36 | steps: 37 | - name: "Check out the repo" 38 | uses: actions/checkout@v4 39 | with: 40 | persist-credentials: false 41 | 42 | - name: "Install Python 3.9" 43 | uses: actions/setup-python@v5 44 | with: 45 | python-version: "3.9" 46 | 47 | - name: "Install requirements" 48 | run: | 49 | python -m pip install numpy cibuildwheel 50 | 51 | - name: "Build wheels" 52 | env: 53 | CIBW_BUILD: cp39* 54 | CIBW_BEFORE_BUILD: python -m pip install numpy 55 | run: | 56 | python -m cibuildwheel --output-dir wheelhouse 57 | ls -al wheelhouse/ 58 | 59 | - name: "Upload wheels" 60 | uses: actions/upload-artifact@v4 61 | with: 62 | name: dist-wheel-${{ matrix.os }} 63 | path: wheelhouse/*.whl 64 | 65 | sdist: 66 | name: "Build source distribution" 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: "Check out the repo" 70 | uses: actions/checkout@v4 71 | with: 72 | persist-credentials: false 73 | 74 | - name: "Install Python 3.9" 75 | uses: actions/setup-python@v5 76 | with: 77 | python-version: "3.9" 78 | 79 | - name: "Install requirements" 80 | run: | 81 | python -m pip install numpy 82 | 83 | - name: "Build sdist" 84 | run: | 85 | python setup.py sdist 86 | ls -al dist/ 87 | 88 | - name: "Upload sdist" 89 | uses: actions/upload-artifact@v4 90 | with: 91 | name: dist-sdist 92 | path: dist/*.tar.gz 93 | -------------------------------------------------------------------------------- /src/aptus/web/static/style.css: -------------------------------------------------------------------------------- 1 | html { height: 100%; } 2 | 3 | html span.mac { display: none; } 4 | 5 | html span.notmac { display: inline; } 6 | 7 | html.mac span.mac { display: inline; } 8 | 9 | html.mac span.notmac { display: none; } 10 | 11 | html.wait { cursor: wait; } 12 | 13 | body { font-family: Helvetica; overflow: hidden; margin: 0; padding: 0; height: 100%; } 14 | 15 | .canvas_container { display: flex; justify-content: center; width: 100%; height: 100%; } 16 | 17 | .canvas_sizer { position: absolute; top: 0; overflow: hidden; } 18 | 19 | canvas.view { position: absolute; image-rendering: crisp-edges; image-rendering: pixelated; } 20 | 21 | canvas.view.wait { cursor: wait; } 22 | 23 | canvas.view.move { cursor: grab; } 24 | 25 | .panel { display: none; position: absolute; overflow: scroll; background: #ffeebb; padding: .5em; border: 2px solid black; border-radius: .5em; box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.4); width: 15em; top: 10em; left: 10em; } 26 | 27 | .panel p { margin: 0 0 1em 0; line-height: 1.3; } 28 | 29 | .panel p.buttons { text-align: right; } 30 | 31 | .panel .closebtn { position: absolute; top: .25em; right: .25em; border: 1px solid #00000000; border-radius: .25em; } 32 | 33 | .panel .closebtn:hover { border: 1px solid black; } 34 | 35 | .panel .closebtn * { margin-bottom: -.3em; } 36 | 37 | .panel.form p { margin: .25em 0 0 0; } 38 | 39 | .panel.form label { display: inline-block; width: 4em; text-align: right; padding-right: .25em; } 40 | 41 | #splash { display: block; padding: 1em 1em 0 1em; opacity: 1; } 42 | 43 | #splash.hidden { visibility: hidden; opacity: 0; transition: opacity 1s, visibility 1s; } 44 | 45 | #helppanel { width: 20em; top: 5em; right: 5em; left: auto; padding: 1em 1em 0 1em; max-height: 80%; } 46 | 47 | #infopanel { width: 18em; top: 5em; left: 5em; } 48 | 49 | #palettepanel { width: 12em; top: auto; bottom: 5em; left: 5em; } 50 | 51 | kbd { display: inline-block; font-family: monospace; font-weight: bold; background: #f0f0f0; border: 2px solid #888; border-color: #888 #333 #333 #888; border-radius: .25em; padding: .1em .25em; margin: .1em; cursor: default; } 52 | 53 | a { text-decoration: none; } 54 | 55 | a:hover { text-decoration: underline; } 56 | 57 | #renderwait { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; background: #00000080; cursor: wait; } 58 | 59 | #renderwait.show { display: block; } 60 | 61 | #renderwait .panel { width: 10em; text-align: center; padding: 2em; top: 20em; left: 0; right: 0; margin: auto; } 62 | -------------------------------------------------------------------------------- /src/aptus/gui/juliapanel.py: -------------------------------------------------------------------------------- 1 | """ A panel to display the Julia set for the currently hovered point in the main 2 | window. 3 | """ 4 | 5 | import wx 6 | 7 | from aptus.gui.computepanel import MiniComputePanel 8 | from aptus.gui.ids import * 9 | from aptus.gui.misc import AptusToolFrame, ListeningWindowMixin 10 | 11 | 12 | class JuliaPanel(MiniComputePanel, ListeningWindowMixin): 13 | """ A panel displaying the Julia set for the current point in another window. 14 | """ 15 | 16 | def __init__(self, parent, viewwin, size=wx.DefaultSize): 17 | """ Create a JuliaPanel, with `parent` as its parent, and `viewwin` as 18 | the window to track. 19 | """ 20 | MiniComputePanel.__init__(self, parent, size=size) 21 | ListeningWindowMixin.__init__(self) 22 | 23 | self.viewwin = viewwin 24 | 25 | self.register_listener(self.on_coloring_changed, EVT_APTUS_COLORING_CHANGED, self.viewwin) 26 | self.register_listener(self.draw_julia, EVT_APTUS_INDICATEPOINT, self.viewwin) 27 | 28 | self.compute.center, self.compute.diam = (0.0,0.0), (3.0,3.0) 29 | self.compute.mode = 'julia' 30 | 31 | self.on_coloring_changed(None) 32 | 33 | # Need to call update_info after the window appears, so that the widths of 34 | # the text controls can be set properly. Else, it all appears left-aligned. 35 | wx.CallAfter(self.draw_julia) 36 | 37 | def draw_julia(self, event=None): 38 | # Different events will trigger this, be flexible about how to get the 39 | # mouse position. 40 | if event and hasattr(event, 'point'): 41 | pt = event.point 42 | else: 43 | pt = self.viewwin.ScreenToClient(wx.GetMousePosition()) 44 | 45 | pt_info = self.viewwin.get_point_info(pt) 46 | if pt_info: 47 | self.compute.rijulia = pt_info['r'], pt_info['i'] 48 | self.compute.iter_limit = 1000 49 | else: 50 | self.compute.rijulia = 0,0 51 | self.compute.create_mandel() 52 | self.computation_changed() 53 | 54 | # Need to let the main window handle the event too. 55 | if event: 56 | event.Skip() 57 | 58 | def on_coloring_changed(self, event_unused): 59 | if self.compute.copy_coloring(self.viewwin.compute): 60 | self.coloring_changed() 61 | 62 | 63 | class JuliaFrame(AptusToolFrame): 64 | def __init__(self, mainframe, viewwin): 65 | AptusToolFrame.__init__(self, mainframe, title='Julia Set', size=(180,180)) 66 | self.panel = JuliaPanel(self, viewwin) 67 | -------------------------------------------------------------------------------- /lab/sugree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # from http://www.howforge.com/mandelbrot-set-viewer-using-wxpython 3 | 4 | import wx 5 | 6 | class MandelbrotSet: 7 | def __init__(self,x0,y0,x1,y1,w,h,limit=2,maxiter=30): 8 | self.x0,self.y0 = x0,y0 9 | self.rx,self.ry = (x1-x0)/w,(y0-y1)/h 10 | self.w,self.h = w,h 11 | 12 | self.limit = limit 13 | self.maxiter = maxiter 14 | 15 | def from_screen(self,x,y): 16 | return self.x0+self.rx*x,self.y0-self.ry*y 17 | 18 | def zoom_in(self,x,y): 19 | zx,zy = self.w/4,self.h/4 20 | return x-zx*self.rx,y+zy*self.ry,x+zx*self.rx,y-zy*self.ry,self.w,self.h 21 | 22 | def zoom_out(self,x,y): 23 | zx,zy = self.w,self.h 24 | return x-zx*self.rx,y+zy*self.ry,x+zx*self.rx,y-zy*self.ry,self.w,self.h 25 | 26 | def is_mandelbrot(self,x,y): 27 | p = complex(x,y) 28 | i = 0 29 | z = 0+0j 30 | while abs(z) < self.limit and i < self.maxiter: 31 | z = z*z+p 32 | i += 1 33 | return i 34 | 35 | def compute(self,callback): 36 | x = self.x0 37 | for xi in range(self.w): 38 | y = self.y0 39 | for yi in range(self.h): 40 | c = self.is_mandelbrot(x,y) 41 | callback(xi,yi,c) 42 | y -= self.ry 43 | x += self.rx 44 | 45 | class wxMandelbrotSetViewer(wx.Frame): 46 | def __init__(self,x0,y0,x1,y1,w,h): 47 | super(wxMandelbrotSetViewer,self).__init__(None,-1,'Mandelbrot Set') 48 | self.dc = None 49 | 50 | self.SetSize((w,h)) 51 | self.bitmap = wx.EmptyBitmap(w,h) 52 | self.panel = wx.Panel(self) 53 | self.panel.Bind(wx.EVT_PAINT,self.on_paint) 54 | self.panel.Bind(wx.EVT_LEFT_UP,self.on_zoom_in) 55 | self.panel.Bind(wx.EVT_RIGHT_UP,self.on_zoom_out) 56 | 57 | self.w,self.h = w,h 58 | self.m = MandelbrotSet(x0,y0,x1,y1,w,h) 59 | 60 | def on_zoom_in(self,event): 61 | x,y = self.m.from_screen(event.GetX(),event.GetY()) 62 | self.m = MandelbrotSet(*self.m.zoom_in(x,y)) 63 | self.dc = None 64 | self.Refresh() 65 | 66 | def on_zoom_out(self,event): 67 | x,y = self.m.from_screen(event.GetX(),event.GetY()) 68 | self.m = MandelbrotSet(*self.m.zoom_out(x,y)) 69 | self.dc = None 70 | self.Refresh() 71 | 72 | def on_paint(self,event): 73 | if not self.dc: 74 | self.dc = self.draw() 75 | dc = wx.PaintDC(self.panel) 76 | dc.Blit(0,0,self.w,self.h,self.dc,0,0) 77 | 78 | def palette(self,c): 79 | c = c*255.0/self.m.maxiter 80 | return map(int,[c,(c+64)%256,(c+32)%256]) 81 | 82 | def draw(self): 83 | dc = wx.MemoryDC() 84 | dc.SelectObject(self.bitmap) 85 | def callback(x,y,c): 86 | r,g,b = self.palette(c) 87 | dc.SetPen(wx.Pen(wx.Colour(r,g,b),1)) 88 | dc.DrawPoint(x,y) 89 | self.m.compute(callback) 90 | return dc 91 | 92 | if __name__ == '__main__': 93 | app = wx.PySimpleApp() 94 | f = wxMandelbrotSetViewer(-2.0,1.0,1.0,-1.0,200,200) 95 | f.Show() 96 | app.MainLoop() 97 | -------------------------------------------------------------------------------- /src/aptus/gui/misc.py: -------------------------------------------------------------------------------- 1 | """ Miscellaneous stuff for the Aptus GUI. 2 | """ 3 | 4 | import wx 5 | from wx.lib.evtmgr import eventManager 6 | 7 | 8 | class AptusToolableFrameMixin: 9 | """ A mixin to add to a frame. Tool windows can be attached to this, and 10 | will behave nicely (minimizing, etc). 11 | """ 12 | def __init__(self): 13 | self.toolwins = [] 14 | self.Bind(wx.EVT_ICONIZE, self.on_iconize) 15 | self.Bind(wx.EVT_CLOSE, self.on_close) 16 | 17 | def add_toolwin(self, toolwin): 18 | self.toolwins.append(toolwin) 19 | 20 | def remove_toolwin(self, toolwin): 21 | self.toolwins.remove(toolwin) 22 | 23 | def on_iconize(self, event): 24 | bshow = not event.Iconized() 25 | for toolwin in self.toolwins: 26 | toolwin.Show(bshow) 27 | event.Skip() 28 | 29 | def on_close(self, event): 30 | for toolwin in self.toolwins: 31 | toolwin.Close() 32 | event.Skip() 33 | 34 | 35 | class AptusToolFrame(wx.MiniFrame): 36 | """ A frame for tool windows. 37 | """ 38 | # This handles getting the styles right for miniframes. 39 | def __init__(self, mainframe, title='', size=wx.DefaultSize): 40 | # If I pass mainframe into MiniFrame, the focus gets messed up, and keys don't work anymore!? Really, where? 41 | wx.MiniFrame.__init__(self, mainframe, title=title, size=size, 42 | style=wx.DEFAULT_FRAME_STYLE # TODO: | wx.TINY_CAPTION_HORIZONTAL 43 | ) 44 | self.mainframe = mainframe 45 | self.mainframe.add_toolwin(self) 46 | self.Bind(wx.EVT_WINDOW_DESTROY, self.on_destroy) 47 | 48 | def on_destroy(self, event_unused): 49 | self.mainframe.remove_toolwin(self) 50 | 51 | 52 | class ListeningWindowMixin: 53 | """ Adds event listening to a window, and deregisters automatically on 54 | destruction. 55 | """ 56 | def __init__(self): 57 | # The eventManager listeners we've registered. 58 | self.listeners = set() 59 | # The raw events we've bound to. 60 | self.events = set() 61 | 62 | self.Bind(wx.EVT_WINDOW_DESTROY, self.on_destroy) 63 | 64 | def on_destroy(self, event_unused): 65 | for l in self.listeners: 66 | eventManager.DeregisterListener(l) 67 | for other_win, evt in self.events: 68 | other_win.Unbind(evt) 69 | 70 | def register_listener(self, fn, evt, sender): 71 | """ Register a listener for an eventManager event. This will be automatically 72 | de-registered when self is destroyed. 73 | """ 74 | eventManager.Register(fn, evt, sender) 75 | self.listeners.add(fn) 76 | 77 | def deregister_listener(self, fn): 78 | """ Deregister a previously registered listener. 79 | """ 80 | eventManager.DeregisterListener(fn) 81 | 82 | if fn in self.listeners: 83 | self.listeners.remove(fn) 84 | 85 | def bind_to_other(self, other_win, evt, fn): 86 | """ Bind to a standard wxPython event on another window. This will be 87 | automatically Unbind'ed when self is destroyed. 88 | """ 89 | other_win.Bind(evt, fn) 90 | self.events.add((other_win, evt)) 91 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Unreleased 2 | ---------- 3 | 4 | * fix: iter_limit wasn't set from the URL parameter. 5 | 6 | * Added lightness tweaking controls. 7 | 8 | * Added "F" for full-screen mode. 9 | 10 | * Web interface now has a favicon. 11 | 12 | Aptus 3.0.1, July 25 2021 13 | ------------------------- 14 | 15 | * Web interface. 16 | 17 | * Requires Python 3.9 or higher. 18 | 19 | Next release (was 2.1, never formally released) 20 | ----------------------------------------------- 21 | 22 | * Multi-threaded. 23 | 24 | * Added full-screen mode. 'f' toggles between full-screen and windowed. 25 | 26 | * Window size can be set interactively with the 'w' key. Enter an explicit size, 27 | or 's/2' (or s/3, s/4, etc) for half the screen size. 28 | 29 | * Un-computed pixels are shown as gray checkerboard. 30 | 31 | * If the view crosses the x-axis, the symmetry is used to speed up computation. 32 | 33 | * 'o' opens saved settings or image files (*.aptus or *.png). 34 | 35 | * Files can be dropped on the Aptus window to open them. 36 | 37 | * The About section of the Help dialog now shows versions of installed 38 | pre-requisite packages. 39 | 40 | 41 | Aptus 2.0, October 2008 42 | ----------------------- 43 | 44 | * Multiple top-level exploration windows. Use 'n' to make a new window. 45 | 46 | * Tool panels show supplementary information: 47 | * You Are Here shows your location in the Mandelbrot set. Use 'l' to show it. 48 | * Palettes panel shows all the palettes, and the one currently in use. 49 | * Statistics panel shows statistics about the latest computation. 50 | * Point Info panel shows information about the current point, shift-hover to indicate point. 51 | * Julia panel shows Julia set for the current point, shift-hover to indicate point. 52 | Double-clicking the Julia panel opens a new exploration window to explore that Julia set. 53 | 54 | * Computation improvements: 55 | * Faster. 56 | * The exploration window updates during computation. 57 | * Continuous coloring is more accurate now: banding artifacts are gone. 58 | * When dragging the exploration window, pixels still in the window aren't re-calculated. 59 | 60 | * Center and diameter can be specified in the command line arguments. 61 | 62 | 63 | Aptus 1.56, April 2008 64 | ---------------------- 65 | 66 | * Yet more painting improvements. Thanks, Paul Ollis. 67 | 68 | 69 | Aptus 1.55, April 2008 70 | ---------------------- 71 | 72 | * Painting is now flicker-free. Thanks, Rob McMullen. 73 | 74 | 75 | Aptus 1.51, March 2008 76 | ---------------------- 77 | 78 | * Painting didn't work at all on Mac or Linux! 79 | 80 | * Some keys didn't respond on Linux. 81 | 82 | * Shifting the palette into negative offsets caused incorrect jumps in colors. 83 | 84 | 85 | Aptus 1.5, March 2008 86 | --------------------- 87 | 88 | * Continuous coloring. 89 | 90 | * Rotation support. 91 | 92 | * Middle mouse button drags the image. 93 | 94 | * Palette tweaking: 95 | * Hue and saturation adjustments. 96 | * Scaling the palette to adjust distance between colors. 97 | 98 | * More statistics: boundaries traced, boundaries filled, and points computed. 99 | 100 | * Statistics are written into the final .PNG files. 101 | 102 | * Reads .xet files from http://hbar.servebeer.com/mandelbrot/coordinates.html 103 | -------------------------------------------------------------------------------- /src/aptus/progress.py: -------------------------------------------------------------------------------- 1 | """ Progress reporters for Aptus. 2 | """ 3 | 4 | import time 5 | 6 | from aptus.timeutil import duration, future 7 | 8 | 9 | class NullProgressReporter: 10 | """ Basic interface for reporting rendering progress. 11 | """ 12 | 13 | def begin(self): 14 | """ Called once at the beginning of a render. 15 | """ 16 | pass 17 | 18 | def progress(self, arg, num_done, info=''): 19 | """ Called repeatedly to report progress. 20 | 21 | `arg` is an opaque argument, the caller can use it for whatever they want. 22 | 23 | `num_done` is a int indicating the count of progress. There is no 24 | defined range for `num_done`, it is assumed that the caller knows what 25 | work is being done, and what the number mean. 26 | 27 | `info` is a string giving some information about what's been done. 28 | 29 | """ 30 | pass 31 | 32 | def end(self): 33 | """ Called once at the end of a render. 34 | """ 35 | pass 36 | 37 | 38 | class IntervalProgressReporter: 39 | """ A progress reporter decorator that only calls its wrapped reporter 40 | every N seconds. 41 | """ 42 | def __init__(self, nsec, reporter): 43 | self.nsec = nsec 44 | self.reporter = reporter 45 | 46 | def begin(self): 47 | self.latest = time.time() 48 | self.reporter.begin() 49 | 50 | def progress(self, arg, num_done, info=''): 51 | now = time.time() 52 | if now - self.latest > self.nsec: 53 | self.reporter.progress(arg, num_done, info) 54 | self.latest = now 55 | 56 | def end(self): 57 | self.reporter.end() 58 | 59 | 60 | class AggregateProgressReporter: 61 | """ Collect a number of progress reporters into a single unified front. 62 | """ 63 | def __init__(self): 64 | self.kids = [] 65 | 66 | def add(self, reporter): 67 | self.kids.append(reporter) 68 | 69 | def begin(self): 70 | for kid in self.kids: 71 | kid.begin() 72 | 73 | def progress(self, arg, num_done, info=''): 74 | for kid in self.kids: 75 | kid.progress(arg, num_done, info) 76 | 77 | def end(self): 78 | for kid in self.kids: 79 | kid.end() 80 | 81 | 82 | # Cheap way to measure and average a number of runs. 83 | nruns = 0 84 | totaltotal = 0 85 | 86 | class ConsoleProgressReporter: 87 | """ A progress reporter that writes lines to the console. 88 | 89 | This `progress` function interprets the `num_done` arg as a fraction, in 90 | millionths. 91 | 92 | """ 93 | def begin(self): 94 | self.start = time.time() 95 | 96 | def progress(self, arg, num_done, info=''): 97 | frac_done = num_done / 1000000.0 98 | now = time.time() 99 | so_far = int(now - self.start) 100 | to_go = int(so_far / frac_done * (1-frac_done)) 101 | if info: 102 | info = ' ' + info 103 | print("%5.2f%%: %11s done, %11s to go, eta %10s%s" % ( 104 | frac_done*100, duration(so_far), duration(to_go), future(to_go), info 105 | )) 106 | 107 | def end(self): 108 | total = time.time() - self.start 109 | global totaltotal, nruns 110 | totaltotal += total 111 | nruns += 1 112 | print("Total: %s (%.4fs)" % (duration(total), total)) 113 | #print("Running average: %.6fs over %d runs" % (totaltotal/nruns, nruns)) 114 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Aptus: A Mandelbrot set explorer and renderer. 3 | 4 | Aptus is a Mandelbrot set explorer and renderer with a browser interface, a wxPython GUI and 5 | a computation extension in C for speed. 6 | 7 | For more information see `Aptus v3 `_. 8 | 9 | """ 10 | 11 | from setuptools import setup 12 | 13 | from distutils.core import Extension 14 | import sys 15 | 16 | try: 17 | import numpy 18 | except: 19 | raise Exception("Need numpy, from http://numpy.scipy.org/") 20 | 21 | version = "3.0.1" 22 | 23 | doclines = __doc__.split("\n") 24 | 25 | classifiers = """ 26 | Development Status :: 5 - Production/Stable 27 | Environment :: Console 28 | Environment :: MacOS X 29 | Environment :: Win32 (MS Windows) 30 | Environment :: X11 Applications :: GTK 31 | Programming Language :: Python :: 3.9 32 | License :: OSI Approved :: MIT License 33 | Programming Language :: C 34 | Programming Language :: Python 35 | Topic :: Artistic Software 36 | Topic :: Scientific/Engineering :: Mathematics 37 | """ 38 | 39 | options = {} 40 | 41 | # Most examples on the web seem to imply that O3 will be automatic, 42 | # but for me it wasn't, and I want all the speed I can get... 43 | extra_compile_args = ["-O3"] 44 | 45 | if sys.platform == "win32": 46 | extra_compile_args = ["-O2"] 47 | 48 | setup( 49 | # The metadata 50 | name="Aptus", 51 | description=doclines[0], 52 | long_description="\n".join(doclines[2:]), 53 | long_description_content_type="text/x-rst", 54 | version=version, 55 | author="Ned Batchelder", 56 | author_email="ned@nedbatchelder.com", 57 | url="http://nedbatchelder.com/code/aptus/v3.html", 58 | license="MIT", 59 | classifiers=list(filter(None, classifiers.split("\n"))), 60 | python_requires=">=3.9", 61 | 62 | project_urls={ 63 | "Documentation": "https://nedbatchelder.com/code/aptus/v3.html", 64 | "Code": "http://github.com/nedbat/aptus", 65 | "Issues": "https://github.com/nedbat/aptus/issues", 66 | "Funding": "https://github.com/users/nedbat/sponsorship", 67 | }, 68 | 69 | # The data 70 | packages=[ 71 | "aptus", 72 | "aptus.gui", 73 | "aptus.web", 74 | ], 75 | 76 | package_dir={ 77 | "": "src", 78 | }, 79 | 80 | package_data={ 81 | "aptus": [ 82 | "*.ico", 83 | "*.png", 84 | "palettes/*.ggr", 85 | "web/static/*.*", 86 | "web/templates/*.*", 87 | ] 88 | }, 89 | 90 | ext_modules=[ 91 | Extension( 92 | "aptus.engine", 93 | sources=["ext/engine.c"], 94 | include_dirs=[numpy.get_include()], 95 | extra_compile_args=extra_compile_args, 96 | ), 97 | ], 98 | 99 | entry_points={ 100 | "console_scripts": [ 101 | "aptuscmd = aptus.cmdline:main", 102 | "aptusgui = aptus.gui:main", 103 | "aptusweb = aptus.web:main", 104 | ], 105 | }, 106 | 107 | setup_requires=[ 108 | "numpy", 109 | ], 110 | 111 | install_requires=[ 112 | "Pillow", 113 | "numpy", 114 | ], 115 | 116 | extras_require={ 117 | "gui": [ 118 | "wxPython", 119 | ], 120 | "web": [ 121 | "aiofiles", 122 | "cachetools", 123 | "Jinja2", 124 | "fastapi", 125 | "uvicorn", 126 | ], 127 | }, 128 | 129 | options=options, 130 | ) 131 | -------------------------------------------------------------------------------- /src/aptus/web/static/style.scss: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | 4 | span.mac { 5 | display: none; 6 | } 7 | 8 | span.notmac { 9 | display: inline; 10 | } 11 | 12 | &.mac { 13 | span.mac { 14 | display: inline; 15 | } 16 | span.notmac { 17 | display: none; 18 | } 19 | 20 | } 21 | 22 | &.wait { 23 | cursor: wait; 24 | } 25 | } 26 | 27 | body { 28 | font-family: Helvetica; 29 | overflow: hidden; 30 | margin: 0; 31 | padding: 0; 32 | height: 100%; 33 | } 34 | 35 | .canvas_container { 36 | display: flex; 37 | justify-content: center; 38 | width: 100%; 39 | height: 100%; 40 | } 41 | 42 | .canvas_sizer { 43 | position: absolute; 44 | top: 0; 45 | overflow: hidden; 46 | } 47 | 48 | canvas.view { 49 | position: absolute; 50 | // Get sharp images in the canvas. Browsers disagree how to do that. 51 | image-rendering: crisp-edges; 52 | image-rendering: pixelated; 53 | 54 | &.wait { 55 | cursor: wait; 56 | } 57 | 58 | &.move { 59 | cursor: grab; 60 | } 61 | } 62 | 63 | .panel { 64 | display: none; 65 | position: absolute; 66 | overflow: scroll; 67 | background: #ffeebb; 68 | padding: .5em; 69 | border: 2px solid black; 70 | border-radius: .5em; 71 | box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.4); 72 | 73 | width: 15em; 74 | top: 10em; 75 | left: 10em; 76 | 77 | p { 78 | margin: 0 0 1em 0; 79 | line-height: 1.3; 80 | 81 | &.buttons { 82 | text-align: right; 83 | } 84 | } 85 | 86 | .closebtn { 87 | position: absolute; 88 | top: .25em; 89 | right: .25em; 90 | border: 1px solid #00000000; 91 | border-radius: .25em; 92 | 93 | &:hover { 94 | border: 1px solid black; 95 | } 96 | 97 | * { 98 | margin-bottom: -.3em; 99 | } 100 | } 101 | 102 | &.form { 103 | p { 104 | margin: .25em 0 0 0; 105 | } 106 | label { 107 | display: inline-block; 108 | width: 4em; 109 | text-align: right; 110 | padding-right: .25em; 111 | } 112 | } 113 | } 114 | 115 | #splash { 116 | display: block; 117 | padding: 1em 1em 0 1em; 118 | opacity: 1; 119 | &.hidden { 120 | visibility: hidden; 121 | opacity: 0; 122 | transition: opacity 1s, visibility 1s; 123 | } 124 | } 125 | 126 | #helppanel { 127 | width: 20em; 128 | top: 5em; 129 | right: 5em; 130 | left: auto; 131 | padding: 1em 1em 0 1em; 132 | max-height: 80%; 133 | } 134 | 135 | #infopanel { 136 | width: 18em; 137 | top: 5em; 138 | left: 5em; 139 | } 140 | 141 | #palettepanel { 142 | width: 12em; 143 | top: auto; 144 | bottom: 5em; 145 | left: 5em; 146 | } 147 | 148 | kbd { 149 | display: inline-block; 150 | font-family: monospace; 151 | font-weight: bold; 152 | background: #f0f0f0; 153 | border: 2px solid #888; 154 | border-color: #888 #333 #333 #888; 155 | border-radius: .25em; 156 | padding: .1em .25em; 157 | margin: .1em; 158 | cursor: default; 159 | } 160 | 161 | a { 162 | text-decoration: none; 163 | 164 | &:hover { 165 | text-decoration: underline; 166 | } 167 | } 168 | 169 | #renderwait { 170 | position: absolute; 171 | top: 0; left: 0; 172 | width: 100%; height: 100%; 173 | display: none; 174 | background: #00000080; 175 | cursor: wait; 176 | &.show { 177 | display: block; 178 | } 179 | .panel { 180 | width: 10em; 181 | text-align: center; 182 | padding: 2em; 183 | top: 20em; 184 | left: 0; 185 | right: 0; 186 | margin: auto; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /lab/point.py: -------------------------------------------------------------------------------- 1 | # Points 2 | 3 | """ A simple pair of numbers that can be manipulated arithmetically. 4 | 5 | >>> p = Point(10,20) 6 | >>> p 7 | 8 | >>> Point(p) 9 | 10 | >>> Point() 11 | 12 | 13 | Points act like 2-tuples: 14 | 15 | >>> len(p) 16 | 2 17 | >>> a, b = p 18 | >>> print a, b 19 | 10 20 20 | 21 | Except that you can change their x and y components: 22 | 23 | >>> p = Point(10, 20) 24 | >>> p.x = 11 25 | >>> p 26 | 27 | 28 | Points can be added (or subtracted, multipled, or divided): 29 | 30 | >>> Point(10,20) + Point(1,2) 31 | 32 | >>> Point(10,20) + (17, 23) 33 | 34 | >>> p = Point(10,20) 35 | >>> p += (1,2) 36 | >>> p 37 | 38 | >>> Point() + 1 39 | 40 | 41 | Error cases: 42 | 43 | >>> Point(1) 44 | Traceback (most recent call last): 45 | TypeError: Don't know how to make a Point from 1 46 | >>> Point() + "hey" 47 | Traceback (most recent call last): 48 | TypeError: Don't know how to add and 'hey' 49 | 50 | """ 51 | 52 | class Point(object): 53 | def __init__(self, *args): 54 | self.x, self.y = 0, 0 55 | if len(args) == 2: 56 | self.x, self.y = args 57 | elif len(args) == 1: 58 | if isinstance(args[0], Point): 59 | self.x, self.y = args[0].x, args[0].y 60 | elif len(args[0]) == 2: 61 | self.x, self.y = args[0] 62 | else: 63 | raise TypeError("Don't know how to make a Point from %r" % args) 64 | elif len(args) == 0: 65 | pass 66 | else: 67 | raise TypeError("Don't know how to make a Point from %r" % args) 68 | 69 | def __repr__(self): 70 | return "" % (self.x, self.y) 71 | 72 | # Methods that make this act like a 2-tuple. 73 | 74 | def __iter__(self): 75 | yield self.x 76 | yield self.y 77 | 78 | def __len__(self): 79 | return 2 80 | 81 | def __getitem__(self, i): 82 | if i == 0: 83 | return self.x 84 | elif i == 1: 85 | return self.y 86 | raise IndexError 87 | 88 | # Methods that make this work like an arithmetic object. 89 | 90 | def get_pair(self, other, op): 91 | if isinstance(other, (Point, tuple, list)): 92 | return other 93 | elif isinstance(other, (int, float)): 94 | return other, other 95 | else: 96 | raise TypeError("Don't know how to %s %r and %r" % (op, self, other)) 97 | 98 | def __add__(self, other): 99 | ox, oy = self.get_pair(other, "add") 100 | return Point(self.x+ox, self.y+oy) 101 | 102 | def __iadd__(self, other): 103 | ox, oy = self.get_pair(other, "add") 104 | self.x += ox 105 | self.y += oy 106 | return self 107 | 108 | def __sub__(self, other): 109 | ox, oy = self.get_pair(other, "subtract") 110 | return Point(self.x-ox, self.y-oy) 111 | 112 | def __isub__(self, other): 113 | ox, oy = self.get_pair(other, "subtract") 114 | self.x -= ox 115 | self.y -= oy 116 | return self 117 | 118 | def __mul__(self, other): 119 | ox, oy = self.get_pair(other, "multiply") 120 | return Point(self.x*ox, self.y*oy) 121 | 122 | def __imul__(self, other): 123 | ox, oy = self.get_pair(other, "multiply") 124 | self.x *= ox 125 | self.y *= oy 126 | return self 127 | 128 | def __div__(self, other): 129 | ox, oy = self.get_pair(other, "divide") 130 | return Point(self.x/ox, self.y/oy) 131 | 132 | def __idiv__(self, other): 133 | ox, oy = self.get_pair(other, "divide") 134 | self.x /= ox 135 | self.y /= oy 136 | return self 137 | 138 | if __name__ == '__main__': 139 | import doctest, sys 140 | doctest.testmod(verbose=('-v' in sys.argv)) 141 | -------------------------------------------------------------------------------- /doc/v3.px: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | Created. 6 | 7 | 8 |

Aptus is a Mandelbrot set viewer and renderer. It is written in Python 9 | with a computation engine in C for speed. 10 |

11 | 12 | 13 | A portion of the Mandelbrot set 14 | 15 | 16 | 17 |

Getting Aptus

18 | 19 |

Installation

20 | 21 |

Aptus requires Python 3.9 or greater.

22 | 23 |

Use pip to install Aptus. The web interface requires the "web" extra:

24 | 25 | python3.9 -m pip install "aptus[web]" 26 | 27 |

Source

28 | 29 |

The source is available on 30 | GitHub if you prefer direct access to the code, including recent 31 | changes.

32 | 33 | 34 |

Using Aptus

35 | 36 |

There are two ways to use Aptus: a browser-based explorer, and a command line 37 | renderer. There is also an older GUI which doesn't work as well as it used to, 38 | but has more features than the browser interface. The browser and GUI let you 39 | zoom in and out, and change the color palette to find an image you like. The 40 | command line renderer produces higher-quality images.

41 | 42 |

Web interface

43 | 44 |

To launch the web interface, use the "aptusweb" command. It starts a local 45 | web server on http://127.0.0.1:8042. Open 46 | that URL in your browser, and you should see a Mandelbrot set.

47 | 48 |

Hitting the "?" key will bring up a help panel, but briefly, click to zoom 49 | in, shift-click to zoom out. Dragging will move the image around, and 50 | shift-dragging will select a new rectangle to view.

51 | 52 |

The "s" key will render a downloadable image with super-sampling for higher 53 | quality.

54 | 55 |

Parameter files

56 | 57 |

When saving an image as a .PNG file, Aptus also stores all its parameter 58 | information in a text block hidden in the image, so that the .PNG can be used 59 | directly as a parameter file for the command line renderer.

60 | 61 |

GUI usage

62 | 63 |

The GUI interface runs with the "aptusgui" command. Details of how to use it 64 | are on the older Aptus page.

65 | 66 | 67 |

Command line usage

68 | 69 |

The command line renderer is called "aptuscmd". It will accept a number of 70 | switches or parameter files: 71 |

72 | 73 | 74 | Usage: aptuscmd [options] [parameterfile] 75 | 76 | Aptus renders Mandelbrot set images. Three flavors are available: aptusweb and 77 | aptusgui for interactive exploration, and aptuscmd for high-quality rendering. 78 | 79 | Options: 80 | -h, --help show this help message and exit 81 | -a ANGLE, --angle=ANGLE 82 | set the angle of rotation 83 | --center=RE,IM set the center of the view 84 | -c, --continuous use continuous coloring 85 | --diam=DIAM set the diameter of the view 86 | -i ITER_LIMIT, --iterlimit=ITER_LIMIT 87 | set the limit on the iteration count 88 | -o OUTFILE, --output=OUTFILE 89 | set the output filename (aptuscmd only) 90 | --phase=PHASE set the palette phase 91 | --pscale=SCALE set the palette scale 92 | -s WIDxHGT, --size=WIDxHGT 93 | set the pixel size of the image 94 | --super=S set the supersample rate (aptuscmd only) 95 | 96 | 97 | 98 |

More samples

99 | 100 | 101 | 102 | 103 |
104 | 105 | 106 |
107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/aptus/gui/palettespanel.py: -------------------------------------------------------------------------------- 1 | """ Palette visualization for Aptus. 2 | """ 3 | 4 | import wx 5 | from wx.lib.scrolledpanel import ScrolledPanel 6 | 7 | from aptus.gui.ids import * 8 | from aptus.gui.misc import AptusToolFrame, ListeningWindowMixin 9 | 10 | 11 | class PaletteWin(wx.Window): 12 | """ A window for displaying a single palette. Handles click events to 13 | change the palette in the view window. 14 | """ 15 | def __init__(self, parent, palette, ipal, viewwin, size=wx.DefaultSize): 16 | wx.Window.__init__(self, parent, size=size) 17 | self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) 18 | self.palette = palette 19 | self.ipal = ipal 20 | self.viewwin = viewwin 21 | self.selected = False 22 | 23 | self.Bind(wx.EVT_PAINT, self.on_paint) 24 | self.Bind(wx.EVT_SIZE, self.on_size) 25 | self.Bind(wx.EVT_LEFT_UP, self.on_left_up) 26 | 27 | def on_paint(self, event_unused): 28 | # Geometry: client size and margin widths. 29 | cw, ch = self.GetClientSize() 30 | mt, mr, mb, ml = 3, 6, 3, 6 31 | 32 | dc = wx.AutoBufferedPaintDC(self) 33 | 34 | # Paint the background. 35 | if self.selected: 36 | color = wx.Colour(128, 128, 128) 37 | else: 38 | color = wx.Colour(255, 255, 255) 39 | 40 | dc.SetPen(wx.TRANSPARENT_PEN) 41 | dc.SetBrush(wx.Brush(color, wx.SOLID)) 42 | dc.DrawRectangle(0, 0, cw, ch) 43 | 44 | # Paint the palette 45 | ncolors = len(self.palette.colors) 46 | width = float(cw-mr-ml-2)/ncolors 47 | for c in range(0, ncolors): 48 | dc.SetPen(wx.TRANSPARENT_PEN) 49 | dc.SetBrush(wx.Brush(wx.Colour(*self.palette.colors[c]), wx.SOLID)) 50 | dc.DrawRectangle(int(c*width)+ml+1, mt+1, int(width+1), ch-mt-mb-2) 51 | 52 | # Paint the black outline 53 | dc.SetPen(wx.BLACK_PEN) 54 | dc.SetBrush(wx.TRANSPARENT_BRUSH) 55 | dc.DrawRectangle(ml, mt, cw-ml-mr, ch-mt-mb) 56 | 57 | def on_size(self, event_unused): 58 | # Since the painting changes everywhere when the width changes, refresh 59 | # on size changes. 60 | self.Refresh() 61 | 62 | def on_left_up(self, event_unused): 63 | # Left click: tell the view window to switch to my palette. 64 | self.viewwin.fire_command(id_set_palette, self.ipal) 65 | 66 | 67 | class PalettesPanel(ScrolledPanel, ListeningWindowMixin): 68 | """ A panel displaying a number of palettes. 69 | """ 70 | def __init__(self, parent, palettes, viewwin, size=wx.DefaultSize): 71 | ScrolledPanel.__init__(self, parent, size=size) 72 | ListeningWindowMixin.__init__(self) 73 | 74 | self.viewwin = viewwin 75 | self.palettes = palettes 76 | self.pal_height = 30 77 | self.selected = -1 78 | 79 | self.palwins = [] 80 | self.sizer = wx.FlexGridSizer(rows=len(self.palettes), cols=1, vgap=0, hgap=0) 81 | for i, pal in enumerate(self.palettes): 82 | palwin = PaletteWin(self, pal, i, viewwin, size=(200, self.pal_height)) 83 | self.sizer.Add(palwin, flag=wx.EXPAND) 84 | self.palwins.append(palwin) 85 | 86 | self.sizer.AddGrowableCol(0) 87 | self.sizer.SetFlexibleDirection(wx.HORIZONTAL) 88 | self.SetSizer(self.sizer) 89 | self.SetAutoLayout(True) 90 | self.SetupScrolling() 91 | 92 | self.register_listener(self.on_coloring_changed, EVT_APTUS_COLORING_CHANGED, self.viewwin) 93 | self.on_coloring_changed(None) 94 | 95 | def on_coloring_changed(self, event_unused): 96 | # When the view window's coloring changes, see if the palette changed. 97 | if self.viewwin.palette_index != self.selected: 98 | # Change which of the palettes is selected. 99 | self.palwins[self.selected].selected = False 100 | self.selected = self.viewwin.palette_index 101 | self.palwins[self.selected].selected = True 102 | self.ScrollChildIntoView(self.palwins[self.selected]) 103 | self.Refresh() 104 | 105 | 106 | class PalettesFrame(AptusToolFrame): 107 | """ The top level frame for the palettes list. 108 | """ 109 | def __init__(self, mainframe, palettes, viewwin): 110 | AptusToolFrame.__init__(self, mainframe, title='Palettes', size=(250, 350)) 111 | self.panel = PalettesPanel(self, palettes, viewwin) 112 | -------------------------------------------------------------------------------- /src/aptus/web/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | import dataclasses 4 | import functools 5 | import io 6 | import os 7 | import pathlib 8 | 9 | import cachetools 10 | import PIL 11 | import pydantic 12 | import uvicorn 13 | 14 | from fastapi import FastAPI, Request, Response 15 | from fastapi.responses import HTMLResponse 16 | from fastapi.staticfiles import StaticFiles 17 | from fastapi.templating import Jinja2Templates 18 | 19 | from aptus import __version__ 20 | from aptus.compute import AptusCompute 21 | from aptus.palettes import Palette, all_palettes 22 | 23 | app = FastAPI() 24 | 25 | HERE = pathlib.Path(__file__).parent 26 | app.mount("/static", StaticFiles(directory=HERE / "static"), name="static") 27 | templates = Jinja2Templates(directory=HERE / "templates") 28 | 29 | @app.get("/", response_class=HTMLResponse) 30 | async def home(request: Request): 31 | context = { 32 | "request": request, 33 | "palettes": [p.spec() for p in all_palettes], 34 | "version": __version__, 35 | } 36 | return templates.TemplateResponse("mainpage.html", context) 37 | 38 | def run_in_executor(f): 39 | # from https://stackoverflow.com/a/53719009/14343 40 | @functools.wraps(f) 41 | def inner(*args, **kwargs): 42 | loop = asyncio.get_running_loop() 43 | return loop.run_in_executor(None, lambda: f(*args, **kwargs)) 44 | return inner 45 | 46 | 47 | @dataclasses.dataclass 48 | class CachedResult: 49 | counts: object # ndarray 50 | stats: dict 51 | 52 | @dataclasses.dataclass 53 | class TileResult: 54 | pixels: bytes 55 | stats: dict 56 | 57 | 58 | # Cache of computed counts. One tile is about 830Kb. 59 | cache_size = int(os.getenv("APTUS_CACHE", "500")) 60 | tile_cache = cachetools.LRUCache(cache_size * 1_000_000, getsizeof=lambda cr: cr.counts.nbytes) 61 | 62 | @run_in_executor 63 | def compute_tile(compute, cachekey): 64 | old = tile_cache.get(cachekey) 65 | if old is None: 66 | compute.compute_array() 67 | stats = compute.stats 68 | tile_cache[cachekey] = CachedResult(counts=compute.counts, stats=stats) 69 | else: 70 | compute.set_counts(old.counts) 71 | stats = old.stats 72 | pix = compute.color_mandel() 73 | im = PIL.Image.fromarray(pix) 74 | fout = io.BytesIO() 75 | compute.write_image(im, fout) 76 | return TileResult(pixels=fout.getvalue(), stats=stats) 77 | 78 | @run_in_executor 79 | def compute_render(compute): 80 | compute.compute_pixels() 81 | pix = compute.color_mandel() 82 | im = PIL.Image.fromarray(pix) 83 | if compute.supersample > 1: 84 | im = im.resize(compute.size, PIL.Image.LANCZOS) 85 | fout = io.BytesIO() 86 | compute.write_image(im, fout) 87 | return fout.getvalue() 88 | 89 | class ComputeSpec(pydantic.BaseModel): 90 | center: tuple[float, float] 91 | diam: tuple[float, float] 92 | size: tuple[int, int] 93 | supersample: int 94 | coords: tuple[int, int, int, int] 95 | angle: float 96 | continuous: bool 97 | iter_limit: int 98 | palette: list 99 | palette_tweaks: dict 100 | 101 | class TileRequest(pydantic.BaseModel): 102 | spec: ComputeSpec 103 | seq: int 104 | 105 | def spec_to_compute(spec): 106 | compute = AptusCompute() 107 | compute.quiet = True 108 | compute.center = spec.center 109 | compute.diam = spec.diam 110 | compute.size = spec.size 111 | compute.supersample = spec.supersample 112 | compute.angle = spec.angle 113 | compute.continuous = spec.continuous 114 | compute.iter_limit = spec.iter_limit 115 | compute.palette = Palette().from_spec(spec.palette) 116 | compute.palette_phase = spec.palette_tweaks.get("phase", 0) 117 | compute.palette_scale = spec.palette_tweaks.get("scale", 1.0) 118 | compute.palette.adjust( 119 | hue=spec.palette_tweaks.get("hue", 0), 120 | saturation=spec.palette_tweaks.get("saturation", 0), 121 | lightness=spec.palette_tweaks.get("lightness", 0), 122 | ) 123 | 124 | supercoords = [v * spec.supersample for v in spec.coords] 125 | gparams = compute.grid_params().subtile(*supercoords) 126 | compute.create_mandel(gparams) 127 | return compute 128 | 129 | @app.post("/tile") 130 | async def tile(req: TileRequest): 131 | spec = req.spec 132 | compute = spec_to_compute(spec) 133 | cachekey = f""" 134 | {spec.center} 135 | {spec.diam} 136 | {spec.size} 137 | {spec.angle} 138 | {spec.continuous} 139 | {spec.iter_limit} 140 | {spec.coords} 141 | """ 142 | results = await compute_tile(compute, cachekey) 143 | data_url = "data:image/png;base64," + base64.b64encode(results.pixels).decode("ascii") 144 | return { 145 | "url": data_url, 146 | "seq": req.seq, 147 | "stats": results.stats, 148 | } 149 | 150 | @app.post("/render") 151 | async def render(spec: ComputeSpec): 152 | compute = spec_to_compute(spec) 153 | data = await compute_render(compute) 154 | return Response(content=data) 155 | 156 | 157 | def main(): 158 | uvicorn.run("aptus.web.server:app", host="127.0.0.1", port=8042, reload=True) 159 | -------------------------------------------------------------------------------- /src/aptus/web/templates/mainpage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Aptus 4 | 5 | 6 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | Rendering... 17 |
18 |
19 | 20 | {% set altnote -%} 21 | * 22 | {%- endset %} 23 | 24 | {% set alt -%} 25 | altoption 26 | {%- endset %} 27 | 28 | {% set closebtn -%} 29 |
{% include "icon_close.svg" %}
30 | {%- endset %} 31 | 32 |
33 | {{closebtn}} 34 |

Aptus  Mandelbrot explorer.

35 |

Type ? for help.

36 |
37 | 38 |
39 | {{closebtn}} 40 |

Aptus {{version}}, Mandelbrot set explorer.

41 | 42 |

Copyright 2007-2024, Ned Batchelder. 43 |
http://nedbatchelder.com/code/aptus/v3.html 44 |

45 | 46 |

47 | a set the angle
48 | c toggle continuous coloring
49 | F toggle full-screen
50 | i set the iteration limit
51 | I toggle the computation parameters panel
52 | L display a permalink
53 | 54 | r redraw
55 | R reset everything
56 | s render and save a super-sampled image
57 | U upload a file
58 | w set the canvas size
59 | ( ) rotate the angle {{altnote}}
60 | < > cycle through palettes
61 | , . cycle the palette one color
62 | ; ' stretch the colors, if continuous {{altnote}}
63 | [ ] adjust the hue {{altnote}}
64 | { } adjust the saturation {{altnote}}
65 | - = adjust the lightness {{altnote}}
66 | 0 (zero) reset all color adjustments
67 | ? toggle help panel
68 |

69 |

70 | click: zoom in {{altnote}}
71 | shift-click: zoom out {{altnote}}
72 | drag: pan the view
73 | shift-drag: select new view area
74 |
75 | {{altnote}} +{{alt}}: just a little
76 |

77 |
78 | 79 |
80 | {{closebtn}} 81 |

82 |

83 |

84 |

85 |

86 |
87 | 88 |
89 | {{closebtn}} 90 |

91 |

92 |

93 |

94 |

95 |

96 |
97 | 98 |
99 | {{closebtn}} 100 |

101 |

102 |

103 |
104 | 105 |
106 | {{closebtn}} 107 | 108 |
109 | 110 |
111 | {{closebtn}} 112 |

UPLOAD

113 |
114 | 115 |
116 | {{closebtn}} 117 |

118 |
119 | 120 | 123 | 124 | -------------------------------------------------------------------------------- /lab/boundary.py: -------------------------------------------------------------------------------- 1 | """ A boundary trace function for Mandelbrot computation. 2 | """ 3 | 4 | import numpy 5 | 6 | def trace_boundary(count_fn, w, h, maxiter, threshold=10000, progress_fn=None): 7 | """ Compute counts for pixels, using a boundary trace technique. 8 | count_fn(x,y) returns the iteration count for a pixel. 9 | Returns a numpy array w by h with iteration counts for each pixel. 10 | Threshold is the minimum count that triggers a boundary trace. Below 11 | this, the expense of the trace outweighs simply computing each pixel. 12 | """ 13 | 14 | DOWN, LEFT, UP, RIGHT = range(4) 15 | turn_right = [LEFT, UP, RIGHT, DOWN] 16 | turn_left = [RIGHT, DOWN, LEFT, UP] 17 | counts = numpy.zeros((h, w), dtype=numpy.uint32) 18 | status = numpy.zeros((h, w), dtype=numpy.uint8) 19 | num_trace = 0 20 | num_pixels = 0 21 | total_pixels = h * w 22 | 23 | for yi in xrange(h): 24 | for xi in xrange(w): 25 | s = status[yi,xi] 26 | if s == 0: 27 | c = count_fn(xi, -yi) 28 | counts[yi,xi] = c 29 | num_pixels += 1 30 | status[yi,xi] = s = 1 31 | else: 32 | c = counts[yi,xi] 33 | 34 | comp_c = c or maxiter 35 | if s == 1 and comp_c >= threshold: 36 | # Start a boundary trace. 37 | status[yi,xi] = 2 38 | curdir = DOWN 39 | curx, cury = xi, yi 40 | orig_pt = (xi, yi) 41 | lastx, lasty = xi, yi 42 | start = True 43 | points = [] 44 | 45 | # Find all the points on the boundary. 46 | while True: 47 | 48 | # Eventually, we reach our starting point. Stop. 49 | if not start and (curx,cury) == orig_pt and curdir == DOWN: 50 | break 51 | 52 | # Move to the next position. If we're off the field, turn left. 53 | if curdir == DOWN: 54 | if cury >= h-1: 55 | curdir = RIGHT 56 | continue 57 | cury += 1 58 | elif curdir == LEFT: 59 | if curx <= 0: 60 | curdir = DOWN 61 | continue 62 | curx -= 1 63 | elif curdir == UP: 64 | if cury <= 0: 65 | curdir = LEFT 66 | continue 67 | cury -= 1 68 | elif curdir == RIGHT: 69 | if curx >= w-1: 70 | curdir = UP 71 | continue 72 | curx += 1 73 | 74 | # Get the count of the next position 75 | if status[cury,curx] == 0: 76 | c2 = count_fn(curx, -cury) 77 | counts[cury,curx] = c2 78 | num_pixels += 1 79 | status[cury,curx] = 1 80 | else: 81 | c2 = counts[cury,curx] 82 | 83 | # If the same color, turn right, else turn left. 84 | if c2 == c: 85 | status[cury,curx] = 2 86 | points.append((curx,cury)) 87 | lastx, lasty = curx, cury 88 | curdir = turn_right[curdir] 89 | else: 90 | curx, cury = lastx, lasty 91 | curdir = turn_left[curdir] 92 | 93 | start = False 94 | 95 | if points: 96 | num_trace += 1 97 | 98 | # Now flood fill the region. The points list has all the boundary 99 | # points, so we only need to fill left and right from each of those. 100 | for ptx, pty in points: 101 | curx = ptx 102 | while True: 103 | curx -= 1 104 | if curx < 0: 105 | break 106 | if status[pty,curx] != 0: 107 | break 108 | counts[pty,curx] = c 109 | num_pixels += 1 110 | status[pty,curx] = 2 111 | curx = ptx 112 | while True: 113 | curx += 1 114 | if curx > w-1: 115 | break 116 | if status[pty,curx] != 0: 117 | break 118 | counts[pty,curx] = c 119 | num_pixels += 1 120 | status[pty,curx] = 2 121 | 122 | progress_fn(float(num_pixels)/total_pixels, info='trace %d' % c) 123 | 124 | progress_fn(float(num_pixels)/total_pixels, info='scan %d' % (yi+1)) 125 | 126 | print "Traced %s boundaries" % num_trace 127 | return counts 128 | -------------------------------------------------------------------------------- /src/aptus/ggr.py: -------------------------------------------------------------------------------- 1 | """ Read Gimp .ggr gradient files. 2 | Ned Batchelder, http://nedbatchelder.com 3 | This code is in the public domain. 4 | """ 5 | 6 | __version__ = '1.0.20070915' 7 | 8 | import colorsys 9 | import math 10 | 11 | 12 | class GimpGradient: 13 | """ Read and interpret a Gimp .ggr gradient file. 14 | """ 15 | def __init__(self, f=None): 16 | if f: 17 | self.read(f) 18 | 19 | class _segment: 20 | pass 21 | 22 | def read(self, f): 23 | """ Read a .ggr file from f (either an open file or a file path). 24 | """ 25 | if isinstance(f, str): 26 | f = open(f) 27 | if f.readline().strip() != "GIMP Gradient": 28 | raise IOError("Not a GIMP gradient file") 29 | line = f.readline().strip() 30 | if not line.startswith("Name: "): 31 | raise IOError("Not a GIMP gradient file") 32 | self.name = line.split(": ", 1)[1] 33 | nsegs = int(f.readline().strip()) 34 | self.segs = [] 35 | for dummy in range(nsegs): 36 | line = f.readline().strip() 37 | seg = self._segment() 38 | (seg.l, seg.m, seg.r, 39 | seg.rl, seg.gl, seg.bl, _, 40 | seg.rr, seg.gr, seg.br, _, 41 | seg.fn, seg.space) = map(float, line.split()) 42 | self.segs.append(seg) 43 | 44 | def color(self, x): 45 | """ Get the color for the point x in the range [0..1). 46 | The color is returned as an rgb triple, with all values in the range 47 | [0..1). 48 | """ 49 | # Find the segment. 50 | for s in self.segs: 51 | if s.l <= x <= s.r: 52 | seg = s 53 | break 54 | else: 55 | # No segment applies! Return black I guess. 56 | return (0,0,0) 57 | 58 | # Normalize the segment geometry. 59 | mid = (seg.m - seg.l)/(seg.r - seg.l) 60 | pos = (x - seg.l)/(seg.r - seg.l) 61 | 62 | # Assume linear (most common, and needed by most others). 63 | if pos <= mid: 64 | f = pos/mid/2 65 | else: 66 | f = (pos - mid)/(1 - mid)/2 + 0.5 67 | 68 | # Find the correct interpolation factor. 69 | if seg.fn == 1: # Curved 70 | f = math.pow(pos, math.log(0.5) / math.log(mid)) 71 | elif seg.fn == 2: # Sinusoidal 72 | f = (math.sin((-math.pi/2) + math.pi*f) + 1)/2 73 | elif seg.fn == 3: # Spherical increasing 74 | f -= 1 75 | f = math.sqrt(1 - f*f) 76 | elif seg.fn == 4: # Spherical decreasing 77 | f = 1 - math.sqrt(1 - f*f) 78 | 79 | # Interpolate the colors 80 | if seg.space == 0: 81 | c = ( 82 | seg.rl + (seg.rr-seg.rl) * f, 83 | seg.gl + (seg.gr-seg.gl) * f, 84 | seg.bl + (seg.br-seg.bl) * f 85 | ) 86 | elif seg.space in (1,2): 87 | hl, sl, vl = colorsys.rgb_to_hsv(seg.rl, seg.gl, seg.bl) 88 | hr, sr, vr = colorsys.rgb_to_hsv(seg.rr, seg.gr, seg.br) 89 | 90 | if seg.space == 1 and hr < hl: 91 | hr += 1 92 | elif seg.space == 2 and hr > hl: 93 | hr -= 1 94 | 95 | c = colorsys.hsv_to_rgb( 96 | (hl + (hr-hl) * f) % 1.0, 97 | sl + (sr-sl) * f, 98 | vl + (vr-vl) * f 99 | ) 100 | return c 101 | 102 | def test_it(): 103 | import sys, wx 104 | 105 | class GgrView(wx.Frame): 106 | def __init__(self, ggr, chunks): 107 | """ Display the ggr file as a strip of colors. 108 | If chunks is non-zero, then also display the gradient quantized 109 | into that many chunks. 110 | """ 111 | super(GgrView, self).__init__(None, -1, 'Ggr: %s' % ggr.name) 112 | self.ggr = ggr 113 | self.chunks = chunks 114 | self.SetSize((600, 100)) 115 | self.panel = wx.Panel(self) 116 | self.panel.Bind(wx.EVT_PAINT, self.on_paint) 117 | self.panel.Bind(wx.EVT_SIZE, self.on_size) 118 | 119 | def on_paint(self, event_unused): 120 | dc = wx.PaintDC(self.panel) 121 | cw_unused, ch = self.GetClientSize() 122 | if self.chunks: 123 | self.paint_some(dc, 0, 0, ch/2) 124 | self.paint_some(dc, self.chunks, ch/2, ch) 125 | else: 126 | self.paint_some(dc, 0, 0, ch) 127 | 128 | def paint_some(self, dc, chunks, y0, y1): 129 | cw, ch_unused = self.GetClientSize() 130 | chunkw = 1 131 | if chunks: 132 | chunkw = (cw // chunks) or 1 133 | for x in range(0, cw, chunkw): 134 | c = [int(255*x) for x in ggr.color(float(x)/cw)] 135 | dc.SetPen(wx.Pen(wx.Colour(*c), 1)) 136 | dc.SetBrush(wx.Brush(wx.Colour(*c), wx.SOLID)) 137 | dc.DrawRectangle(x, y0, chunkw, y1-y0) 138 | 139 | def on_size(self, event_unused): 140 | self.Refresh() 141 | 142 | app = wx.PySimpleApp() 143 | ggr = GimpGradient(sys.argv[1]) 144 | chunks = 0 145 | if len(sys.argv) > 2: 146 | chunks = int(sys.argv[2]) 147 | f = GgrView(ggr, chunks) 148 | f.Show() 149 | app.MainLoop() 150 | 151 | if __name__ == '__main__': 152 | test_it() 153 | -------------------------------------------------------------------------------- /lab/test_boundary.py: -------------------------------------------------------------------------------- 1 | """ Unit tests for the boundary tracer. 2 | """ 3 | 4 | from boundary import trace_boundary 5 | import unittest 6 | import numpy 7 | 8 | class BoundaryTest(unittest.TestCase): 9 | 10 | def prepare_picture(self, picture): 11 | lines = picture.split() 12 | # Make sure it's really a rectangle. 13 | assert [len(l) for l in lines] == [len(lines[0])]*len(lines) 14 | return lines, len(lines[0]), len(lines) 15 | 16 | def count_fn_from_picture(self, picture): 17 | lines, _, _ = self.prepare_picture(picture) 18 | self.fn_calls = 0 19 | def fn(x,y): 20 | self.fn_calls += 1 21 | pic = lines[-y][x] 22 | if pic == '.': 23 | return 0 24 | else: 25 | return ord(pic) 26 | return fn 27 | 28 | def assert_correct(self, counts, lines, xo, yo): 29 | y, x = counts.shape 30 | out = [] 31 | for yi in range(y): 32 | l = '' 33 | for xi in range(x): 34 | l += chr(counts[yi,xi] or ord('.')) 35 | out.append(l) 36 | 37 | self.assertEqual(xo, x) 38 | self.assertEqual(yo, y) 39 | if out != lines: 40 | print "Out:" 41 | print "\n".join(out) 42 | print "Lines:" 43 | print "\n".join(lines) 44 | self.assertEqual(out, lines) 45 | 46 | def try_picture(self, picture, num_calls=0): 47 | lines, x, y = self.prepare_picture(picture) 48 | cfn = self.count_fn_from_picture(picture) 49 | counts = trace_boundary(cfn, x, y) 50 | self.assert_correct(counts, lines, x, y) 51 | if num_calls: 52 | self.assertEqual(self.fn_calls, num_calls) 53 | 54 | def try_mand_file(self, fname): 55 | from mand import MandState 56 | ms = MandState() 57 | ms.read(fname) 58 | counts = numpy.fromstring(ms.counts, dtype=numpy.uint32) 59 | counts = counts.reshape((ms.w,ms.h)) 60 | 61 | def fn(x,y): 62 | return counts[-y,x] 63 | 64 | counts2 = trace_boundary(fn, counts.shape[1], counts.shape[0]) 65 | wrong_count = numpy.sum(numpy.logical_not(numpy.equal(counts, counts2))) 66 | print wrong_count 67 | 68 | 69 | def testTestCode(self): 70 | cfn = self.count_fn_from_picture(""" 71 | abcdefghi 72 | xyz012345 73 | """) 74 | self.assertEqual(cfn(0,0), 97) 75 | self.assertEqual(cfn(1,0), 98) 76 | self.assertEqual(cfn(0,1), 120) 77 | self.assertEqual(self.fn_calls, 3) 78 | 79 | def test1(self): 80 | self.try_picture(""" 81 | XXXXXX 82 | XXXXXX 83 | XXXXXX 84 | XXXXXX 85 | """, num_calls=16) 86 | 87 | def test2(self): 88 | self.try_picture(""" 89 | XXXXXX 90 | XXXXXX 91 | XXYYXX 92 | XXYYXX 93 | """, num_calls=24) 94 | 95 | def test3(self): 96 | self.try_picture(""" 97 | XXXXXA 98 | XXXXXB 99 | XXYZXC 100 | XXYZXD 101 | JKLMNE 102 | """, num_calls=30) 103 | 104 | def testMandelbrot(self): 105 | self.try_picture(""" 106 | ~~~~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}||||||||{{{zyvrwuW{|||||}}}}}}~~~~~~~~~~~~ 107 | ~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}|||||||||{{{zyxwoaqwxz{{{|||||}}}}}}~~~~~~~~~ 108 | ~~~~~~~~}}}}}}}}}}}}}}}}}}}|||||||||{{zzzyxvn....Knwyz{{{{||||}}}}}}~~~~~~~ 109 | ~~~~~~}}}}}}}}}}}}}}}}}}||||||||{{zyxuxxxwvuq.....svwwyzzzyr{||}}}}}}}~~~~~ 110 | ~~~~}}}}}}}}}}}}}}}}}|||||{{{{{zzzxt>..qf.............pttfqeqz{|}}}}}}}}~~~ 111 | ~~~}}}}}}}}}}}}}}|||{{{{{{{{{zzzywotn.....................atyz{||}}}}}}}}~~ 112 | ~~}}}}}}}}}||||{{zwvyyyyyyyyyyyxvsP........................swvz{||}}}}}}}}~ 113 | ~}}}}|||||||{{{{zyxvpN[ur]spvwwvi...........................qxz{|||}}}}}}}} 114 | ~}||||||||{{{{{zyytun.........qq............................avz{|||}}}}}}}} 115 | ~||||||{zzzzyyxtroqb...........a............................xz{{|||}}}}}}}} 116 | ~@G::#.6#.(..............................................pvxyz{{||||}}}}}}} 117 | ~||||||{zzzzyyxtroqb...........a............................xz{{|||}}}}}}}} 118 | ~}||||||||{{{{{zyytun.........qq............................avz{|||}}}}}}}} 119 | ~}}}}|||||||{{{{zyxvpN[ur]spvwwvi...........................qxz{|||}}}}}}}} 120 | ~~}}}}}}}}}||||{{zwvyyyyyyyyyyyxvsP........................swvz{||}}}}}}}}~ 121 | ~~~}}}}}}}}}}}}}}|||{{{{{{{{{zzzywotn.....................atyz{||}}}}}}}}~~ 122 | ~~~~}}}}}}}}}}}}}}}}}|||||{{{{{zzzxt>..qf.............pttfqeqz{|}}}}}}}}~~~ 123 | ~~~~~~}}}}}}}}}}}}}}}}}}||||||||{{zyxuxxxwvuq.....svwwyzzzyr{||}}}}}}}~~~~~ 124 | ~~~~~~~~}}}}}}}}}}}}}}}}}}}|||||||||{{zzzyxvn....Knwyz{{{{||||}}}}}}~~~~~~~ 125 | ~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}|||||||||{{{zyxwoaqwxz{{{|||||}}}}}}~~~~~~~~~ 126 | ~~~~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}||||||||{{{zyvrwuW{|||||}}}}}}~~~~~~~~~~~~ 127 | ~~~~~~~~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}}|||||{zmt{{{||||}}}}}~~~~~~~~~~~~~~~~ 128 | """) 129 | 130 | def testBigForReal(self): 131 | self.try_mand_file('test_data\\twelve_wrong.mand') 132 | 133 | if __name__ == '__main__': 134 | unittest.main() 135 | -------------------------------------------------------------------------------- /src/aptus/gui/help.py: -------------------------------------------------------------------------------- 1 | """ Help dialog for Aptus. 2 | """ 3 | 4 | import webbrowser 5 | import sys 6 | 7 | import numpy 8 | import wx 9 | import wx.html2 10 | import wx.lib.layoutf 11 | from PIL import Image 12 | 13 | from aptus import data_file, __version__ 14 | from aptus.options import AptusOptions 15 | 16 | 17 | class HtmlDialog(wx.Dialog): 18 | """ A simple dialog for displaying HTML, with clickable links that launch 19 | a web browser, or change the page displayed in the dialog. 20 | """ 21 | def __init__(self, parent, caption, pages, subs=None, 22 | pos=wx.DefaultPosition, size=(500,530), 23 | style=wx.DEFAULT_DIALOG_STYLE): 24 | wx.Dialog.__init__(self, parent, -1, caption, pos, size, style) 25 | if pos == (-1, -1): 26 | self.CenterOnScreen(wx.BOTH) 27 | 28 | self.pages = pages 29 | self.subs = subs or {} 30 | self.html = wx.html2.WebView.New(self) 31 | self.html.Bind(wx.html2.EVT_WEBVIEW_NAVIGATING, self.on_navigating) 32 | ok = wx.Button(self, wx.ID_OK, "OK") 33 | ok.SetDefault() 34 | 35 | lc = wx.lib.layoutf.Layoutf('t=t#1;b=t5#2;l=l#1;r=r#1', (self,ok)) 36 | self.html.SetConstraints(lc) 37 | self.set_page('interactive') 38 | 39 | lc = wx.lib.layoutf.Layoutf('b=b5#1;r=r5#1;w!80;h*', (self,)) 40 | ok.SetConstraints(lc) 41 | 42 | self.SetAutoLayout(1) 43 | self.Layout() 44 | 45 | def on_navigating(self, event): 46 | url = event.GetURL() 47 | if url == "": 48 | event.Veto() 49 | elif url.startswith(("http:", "https:")): 50 | webbrowser.open(url) 51 | event.Veto() 52 | elif url.startswith('internal:'): 53 | self.set_page(url.split(':')[1]) 54 | 55 | def set_page(self, pagename): 56 | html = self.pages['head'] + self.pages[pagename] 57 | html = html % self.subs 58 | self.html.SetPage(html, "") 59 | 60 | 61 | # The help text 62 | 63 | is_mac = ('wxMac' in wx.PlatformInfo) 64 | 65 | TERMS = { 66 | 'ctrl': 'cmd' if is_mac else 'ctrl', 67 | 'iconsrc': data_file('icon48.png'), 68 | 'version': __version__, 69 | 'python_version': sys.version, 70 | 'wx_version': wx.__version__, 71 | 'numpy_version': numpy.__version__, 72 | 'pil_version': Image.__version__, 73 | } 74 | 75 | 76 | HELP_PAGES = { 77 | 'head': """\ 78 | 89 | 90 | 91 | 92 | 93 | 98 | 99 |
94 | Aptus %(version)s, Mandelbrot set explorer.
95 | Copyright 2007-2020, Ned Batchelder.
96 | http://nedbatchelder.com/code/aptus 97 |
100 | 101 |

102 | Interactive | 103 | Command line | 104 | About

105 |
106 | """, 107 | 108 | 'interactive': """ 109 |

Interactive controls:

110 | 111 |
112 | a: set the angle of rotation.
113 | c: toggle continuous coloring.
114 | f: toggle full-screen display.
115 | h or ?: show this help.
116 | i: set the limit on iterations.
117 | j: jump among a few pre-determined locations.
118 | n: create a new window.
119 | o: open a saved settings or image file.
120 | r: redraw the current image.
121 | s: save the current image or settings.
122 | w: set the window size.
123 | < or >: switch to the next palette.
124 | , or .: cycle the current palette one color.
125 | ; or ': stretch the palette colors (+%(ctrl)s: just a little), if continuous.
126 | [ or ]: adjust the hue of the palette (+%(ctrl)s: just a little).
127 | { or }: adjust the saturation of the palette (+%(ctrl)s: just a little).
128 | 0 (zero): reset all palette adjustments.
129 | space: drag mode: click to drag the image to a new position.
130 | shift: indicate a point of interest for Julia set and point info.
131 | left-click: zoom in (+%(ctrl)s: just a little).
132 | right-click: zoom out (+%(ctrl)s: just a little).
133 | left-drag: select a new rectangle to display.
134 | middle-drag: drag the image to a new position.
135 |
136 | 137 |

Tool windows: press a key to toggle on and off:

138 | 139 |
140 | J (shift-j): Show a Julia set for the current (shift-hovered) point.
141 | l (ell): Show zoom snapshots indicating the current position.
142 | p: Show a list of palettes that can be applied to the current view.
143 | q: Show point info for the current (shift-hovered) point.
144 | v: Show statistics for the latest calculation. 145 |
146 | """, 147 | 148 | 'command': """ 149 |

On the command line, use --help to see options:

150 |
""" + AptusOptions(None).options_help() + "
", 151 | 152 | 'about': """ 153 |

Built with 154 | Python, wxPython, 155 | numpy, and 156 | PIL.

157 | 158 |

Thanks to Rob McMullen and Paul Ollis for help with the drawing code.

159 | 160 |
161 |

Installed versions:

162 |

163 | Aptus: %(version)s
164 | Python: %(python_version)s
165 | wx: %(wx_version)s
166 | numpy: %(numpy_version)s
167 | PIL: %(pil_version)s 168 |

169 | """, 170 | } 171 | 172 | 173 | class HelpDlg(HtmlDialog): 174 | """ The help dialog for Aptus. 175 | """ 176 | def __init__(self, parent): 177 | HtmlDialog.__init__(self, parent, "Aptus", HELP_PAGES, subs=TERMS, size=(650,530)) 178 | -------------------------------------------------------------------------------- /src/aptus/gui/computepanel.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import wx 3 | 4 | from aptus import settings 5 | from aptus.compute import AptusCompute 6 | from aptus.gui.ids import * 7 | from aptus.options import AptusState 8 | from aptus.palettes import all_palettes 9 | from aptus.progress import NullProgressReporter 10 | 11 | 12 | class ComputePanel(wx.Panel): 13 | """ A panel capable of drawing a Mandelbrot. 14 | """ 15 | def __init__(self, parent, size=wx.DefaultSize): 16 | wx.Panel.__init__(self, parent, style=wx.NO_BORDER+wx.WANTS_CHARS, size=size) 17 | self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) 18 | 19 | self.compute = AptusCompute() 20 | self.compute.quiet = True # default to quiet. 21 | self.compute.need_chex = True # GUI wants a checkerboard background. 22 | 23 | # AptusCompute default values 24 | self.compute.palette = all_palettes[0] 25 | 26 | # Bind events 27 | self.Bind(wx.EVT_WINDOW_CREATE, self.on_window_create) 28 | self.Bind(wx.EVT_PAINT, self.on_paint) 29 | self.Bind(wx.EVT_SIZE, self.on_size) 30 | self.Bind(wx.EVT_IDLE, self.on_idle) 31 | 32 | def set_geometry(self, center=None, diam=None, corners=None): 33 | """ Change the panel to display a new place in the Set. 34 | `center` is the ri coords of the new center, `diam` is the r and i 35 | size of the view, `corners` is a 4-tuple (ulr, uli, lrr, lri) of the 36 | four corners of the view. Only specify a subset of these. 37 | """ 38 | compute = self.compute 39 | if corners: 40 | ulr, uli, lrr, lri = corners 41 | compute.center = ((ulr+lrr)/2, (uli+lri)/2) 42 | ulx, uly = compute.pixel_from_coords(ulr, uli) 43 | lrx, lry = compute.pixel_from_coords(lrr, lri) 44 | compute.diam = (abs(compute.pixsize*(lrx-ulx)), abs(compute.pixsize*(lry-uly))) 45 | if center: 46 | compute.center = center 47 | if diam: 48 | compute.diam = diam 49 | 50 | self.geometry_changed() 51 | 52 | # GUI helpers 53 | 54 | def fire_command(self, cmdid, data=None): 55 | # I'm not entirely sure about why this is the right event type to use, 56 | # but it works... 57 | evt = wx.CommandEvent(wx.wxEVT_COMMAND_TOOL_CLICKED) 58 | evt.SetId(cmdid) 59 | evt.SetClientData(data) 60 | wx.PostEvent(self, evt) 61 | 62 | def fire_event(self, evclass, **kwargs): 63 | evt = evclass(**kwargs) 64 | self.GetEventHandler().ProcessEvent(evt) 65 | 66 | def message(self, msg): 67 | top = self.GetTopLevelParent() 68 | top.message(msg) 69 | 70 | def coloring_changed(self): 71 | self.bitmap = None 72 | self.Refresh() 73 | self.fire_event(AptusColoringChangedEvent) 74 | 75 | def computation_changed(self): 76 | self.set_view() 77 | self.fire_event(AptusComputationChangedEvent) 78 | 79 | def geometry_changed(self): 80 | self.set_view() 81 | self.fire_event(AptusGeometryChangedEvent) 82 | 83 | # Event handlers 84 | 85 | def on_window_create(self, event): 86 | self.on_idle(event) 87 | 88 | def on_size(self, event_unused): 89 | self.check_size = True 90 | 91 | def on_idle(self, event_unused): 92 | if self.check_size and self.GetClientSize() != self.compute.size: 93 | if self.GetClientSize() != (0,0): 94 | self.geometry_changed() 95 | 96 | def on_paint(self, event_unused): 97 | if not self.bitmap: 98 | self.bitmap = self.draw_bitmap() 99 | 100 | dc = wx.AutoBufferedPaintDC(self) 101 | dc.DrawBitmap(self.bitmap, 0, 0, False) 102 | self.on_paint_extras(dc) 103 | 104 | def on_paint_extras(self, dc): 105 | """ An overridable method so that derived classes can paint extra stuff 106 | on top of the fractal. 107 | """ 108 | pass 109 | 110 | # Information methods 111 | 112 | def get_stats(self): 113 | """ Return a dictionary full of statistics about the latest computation. 114 | """ 115 | return self.compute.stats 116 | 117 | def get_point_info(self, pt): 118 | """ Return a dictionary of information about the specified point (in client pixels). 119 | If the point is outside the window, None is returned. 120 | """ 121 | if not self.GetRect().Contains(pt): 122 | return None 123 | 124 | x, y = pt 125 | r, i = self.compute.coords_from_pixel(x, y) 126 | 127 | if self.compute.pix is not None: 128 | rgb = self.compute.pix[y, x] 129 | color = "#%02x%02x%02x" % (rgb[0], rgb[1], rgb[2]) 130 | else: 131 | color = None 132 | 133 | count = self.compute.counts[y, x] 134 | if self.compute.eng.cont_levels != 1: 135 | count /= self.compute.eng.cont_levels 136 | 137 | point_info = { 138 | 'x': x, 'y': y, 139 | 'r': r, 'i': i, 140 | 'count': count, 141 | 'color': color, 142 | } 143 | 144 | return point_info 145 | 146 | # Output methods 147 | 148 | def make_progress_reporter(self): 149 | """ Create a progress reporter for use when this panel computes. 150 | """ 151 | return NullProgressReporter() 152 | 153 | def bitmap_from_compute(self): 154 | pix = self.compute.color_mandel() 155 | bitmap = wx.Bitmap.FromBuffer(pix.shape[1], pix.shape[0], pix) 156 | return bitmap 157 | 158 | def draw_bitmap(self): 159 | """ Return a bitmap with the image to display in the window. 160 | """ 161 | wx.BeginBusyCursor() 162 | self.compute.progress = self.make_progress_reporter() 163 | self.compute.while_waiting = self.draw_progress 164 | self.compute.compute_pixels() 165 | wx.CallAfter(self.fire_event, AptusRecomputedEvent) 166 | self.Refresh() 167 | bitmap = self.bitmap_from_compute() 168 | wx.EndBusyCursor() 169 | #print("Parent is active: %r" % self.GetParent().IsActive()) 170 | return bitmap 171 | 172 | def draw_progress(self): 173 | """ Called from the GUI thread periodically during computation. 174 | 175 | Repaints the window. 176 | 177 | """ 178 | self.bitmap = self.bitmap_from_compute() 179 | self.Refresh() 180 | self.Update() 181 | wx.CallAfter(self.fire_event, AptusRecomputedEvent) 182 | wx.SafeYield(onlyIfNeeded=True) 183 | 184 | def set_view(self): 185 | self.bitmap = None 186 | self.compute.size = self.GetClientSize() 187 | self.compute.create_mandel() 188 | self.check_size = False 189 | self.Refresh() 190 | 191 | # Output-writing methods 192 | 193 | def write_png(self, pth): 194 | """ Write the current image as a PNG to the path `pth`. 195 | """ 196 | image = self.bitmap.ConvertToImage() 197 | im = Image.new('RGB', (image.GetWidth(), image.GetHeight())) 198 | im.frombytes(bytes(image.GetData())) 199 | self.compute.write_image(im, pth) 200 | 201 | def write_aptus(self, pth): 202 | """ Write the current Aptus state of the panel to the path `pth`. 203 | """ 204 | aptst = AptusState(self.compute) 205 | aptst.write(pth) 206 | 207 | 208 | class MiniComputePanel(ComputePanel): 209 | """ A compute panel for use as a minor pane. 210 | """ 211 | def __init__(self, *args, **kwargs): 212 | ComputePanel.__init__(self, *args, **kwargs) 213 | 214 | self.Bind(wx.EVT_LEFT_DCLICK, self.on_left_dclick) 215 | 216 | def on_left_dclick(self, event_unused): 217 | """ Double-clicking on a mini compute panel opens a new window to the same 218 | view. 219 | """ 220 | wx.GetApp().new_window(compute=self.compute, size=settings.explorer_size) 221 | -------------------------------------------------------------------------------- /src/aptus/options.py: -------------------------------------------------------------------------------- 1 | """ Options handling for Aptus. 2 | """ 3 | 4 | import json 5 | import optparse 6 | import re 7 | 8 | from PIL import Image 9 | 10 | from aptus.palettes import Palette 11 | 12 | description = """\ 13 | Aptus renders Mandelbrot set images. Three flavors are available: 14 | aptusweb and aptusgui for interactive exploration, and aptuscmd for 15 | high-quality rendering. 16 | """.replace('\n', ' ') 17 | 18 | 19 | class AptusOptions: 20 | """ An option parser for Aptus states. 21 | """ 22 | 23 | def __init__(self, target): 24 | """ Create an AptusOptions parser. Attributes are set on the target, which 25 | should be an AptusCompute-like thing. 26 | """ 27 | self.target = target 28 | 29 | def _create_parser(self): 30 | parser = optparse.OptionParser( 31 | usage="%prog [options] [parameterfile]", 32 | description=description 33 | ) 34 | parser.add_option("-a", "--angle", dest="angle", help="set the angle of rotation") 35 | parser.add_option("--center", dest="center", help="set the center of the view", metavar="RE,IM") 36 | parser.add_option("-c", "--continuous", dest="continuous", help="use continuous coloring", action="store_true") 37 | parser.add_option("--diam", dest="diam", help="set the diameter of the view") 38 | parser.add_option("-i", "--iterlimit", dest="iter_limit", help="set the limit on the iteration count") 39 | parser.add_option("-o", "--output", dest="outfile", help="set the output filename (aptuscmd only)") 40 | parser.add_option("--phase", dest="palette_phase", help="set the palette phase", metavar="PHASE") 41 | parser.add_option("--pscale", dest="palette_scale", help="set the palette scale", metavar="SCALE") 42 | parser.add_option("-s", "--size", dest="size", help="set the pixel size of the image", metavar="WIDxHGT") 43 | parser.add_option("--super", dest="supersample", 44 | help="set the supersample rate (aptuscmd only)", metavar="S") 45 | return parser 46 | 47 | def _pair(self, s, cast): 48 | """ Convert a string argument to a pair of other casted values. 49 | """ 50 | vals = list(map(cast, re.split("[,x]", s))) 51 | if len(vals) == 1: 52 | vals = vals*2 53 | return vals 54 | 55 | def _int_pair(self, s): 56 | """ Convert a string argument to a pair of ints. 57 | """ 58 | return self._pair(s, int) 59 | 60 | def _float_pair(self, s): 61 | """ Convert a string argument to a pair of floats. 62 | """ 63 | return self._pair(s, float) 64 | 65 | def read_args(self, argv): 66 | """ Read aptus options from the provided argv. 67 | """ 68 | parser = self._create_parser() 69 | options, args = parser.parse_args(argv) 70 | 71 | if len(args) > 0: 72 | self.opts_from_file(args[0]) 73 | 74 | if options.angle: 75 | self.target.angle = float(options.angle) 76 | if options.center: 77 | self.target.center = self._float_pair(options.center) 78 | if options.continuous: 79 | self.target.continuous = options.continuous 80 | if options.diam: 81 | self.target.diam = self._float_pair(options.diam) 82 | if options.iter_limit: 83 | self.target.iter_limit = int(options.iter_limit) 84 | if options.outfile: 85 | self.target.outfile = options.outfile 86 | if options.palette_phase: 87 | self.target.palette_phase = int(options.palette_phase) 88 | if options.palette_scale: 89 | self.target.palette_scale = float(options.palette_scale) 90 | if options.size: 91 | self.target.size = self._int_pair(options.size) 92 | if options.supersample: 93 | self.target.supersample = int(options.supersample) 94 | 95 | def options_help(self): 96 | """ Return the help text about the command line options. 97 | """ 98 | parser = self._create_parser() 99 | return parser.format_help() 100 | 101 | def opts_from_file(self, fname): 102 | """ Read aptus options from the given filename. Various forms of input 103 | file are supported. 104 | """ 105 | if fname.endswith('.aptus'): 106 | aptst = AptusState(self.target) 107 | aptst.read(fname) 108 | 109 | elif fname.endswith('.xpf'): 110 | xaos = XaosState() 111 | xaos.read(fname) 112 | self.target.center = xaos.center 113 | self.target.diam = xaos.diam 114 | self.target.angle = xaos.angle 115 | self.target.iter_limit = xaos.maxiter 116 | self.target.palette = xaos.palette 117 | self.target.palette_phase = xaos.palette_phase 118 | 119 | elif fname.endswith('.png'): 120 | im = Image.open(fname) 121 | if "Aptus State" in im.info: 122 | aptst = AptusState(self.target) 123 | aptst.read_string(im.info["Aptus State"]) 124 | else: 125 | raise Exception("PNG file has no Aptus state information: %s" % fname) 126 | 127 | else: 128 | raise Exception("Don't know how to read options from %s" % fname) 129 | 130 | 131 | class AptusStateError(Exception): 132 | pass 133 | 134 | 135 | class AptusState: 136 | """ A serialization class for the state of an Aptus rendering. 137 | The result is a JSON representation. 138 | """ 139 | def __init__(self, target): 140 | self.target = target 141 | 142 | def write(self, f): 143 | if isinstance(f, str): 144 | f = open(f, "w") 145 | f.write(self.write_string()) 146 | 147 | simple_attrs = "center diam angle iter_limit palette_phase palette_scale supersample continuous mode".split() 148 | julia_attrs = "rijulia".split() 149 | 150 | def write_attrs(self, d, attrs): 151 | for a in attrs: 152 | d[a] = getattr(self.target, a) 153 | 154 | def write_string(self): 155 | d = {'Aptus State':1} 156 | 157 | self.write_attrs(d, self.simple_attrs) 158 | d['size'] = list(self.target.size) 159 | d['palette'] = self.target.palette.spec() 160 | 161 | if self.target.mode == 'julia': 162 | self.write_attrs(d, self.julia_attrs) 163 | 164 | return json.dumps(d) 165 | 166 | def read_attrs(self, d, attrs): 167 | for a in attrs: 168 | if a in d: 169 | setattr(self.target, a, d[a]) 170 | 171 | def read(self, f): 172 | if isinstance(f, str): 173 | f = open(f, 'r') 174 | return self.read_string(f.read()) 175 | 176 | def read_string(self, s): 177 | d = json.loads(s) 178 | self.read_attrs(d, self.simple_attrs) 179 | self.target.palette = Palette().from_spec(d['palette']) 180 | self.target.size = d['size'] 181 | self.read_attrs(d, self.julia_attrs) 182 | 183 | 184 | class XaosState: 185 | """ The state of a Xaos rendering. 186 | """ 187 | def __init__(self): 188 | self.maxiter = 170 189 | self.center = -0.75, 0.0 190 | self.diam = 2.55, 2.55 191 | self.angle = 0.0 192 | self.palette_phase = 0 193 | self.palette = Palette().xaos() 194 | 195 | def read(self, f): 196 | if isinstance(f, str): 197 | f = open(f) 198 | for l in f: 199 | if l.startswith('('): 200 | argv = l[1:-2].split() 201 | if hasattr(self, 'handle_'+argv[0]): 202 | getattr(self, 'handle_'+argv[0])(*argv) 203 | 204 | def handle_maxiter(self, op_unused, maxiter): 205 | self.maxiter = int(maxiter) 206 | 207 | def handle_view(self, op_unused, cr, ci, rr, ri): 208 | # Xaos writes i coordinates inverted. 209 | self.center = self.read_float(cr), -self.read_float(ci) 210 | self.diam = self.read_float(rr), self.read_float(ri) 211 | 212 | def handle_shiftpalette(self, op_unused, phase): 213 | self.palette_phase = int(phase) 214 | 215 | def handle_angle(self, op_unused, angle): 216 | self.angle = self.read_float(angle) 217 | 218 | def read_float(self, fstr): 219 | # Xaos writes out floats with extra characters tacked on the end sometimes. 220 | # Very ad-hoc: try converting to float, and if it fails, chop off trailing 221 | # chars until it works. 222 | while True: 223 | try: 224 | return float(fstr) 225 | except ValueError: 226 | fstr = fstr[:-1] 227 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | * V1 2 | 3 | + License. 4 | + output filename for aptuscmd. 5 | - Sample .aptus files. 6 | - Doc pages (rst2px?) 7 | + safeeval or json. (safe_eval) 8 | + Keys working on Linux. 9 | + Cursors working on Linux. 10 | + Cursors for Mac. 11 | + Mac: use apple-key rather than control. 12 | + Help. 13 | + Engine extension should be nested in aptus package. 14 | + Windows kits pre-built. 15 | 16 | * V3 17 | 18 | - Multi-threading 19 | + Fix progress reporting: compute_array thinks it knows the total pixels. 20 | + Proper thread pool for multiple drawing windows. 21 | + Prevent recursive Yield calls that bork the UI. 22 | - Keep the UI responsive while computing? 23 | + Switching away from window in progress then switching back doesn't show updates. 24 | + Update stats window as drawing progresses. 25 | + Better choice of tiles: 26 | tall if crossing the x axis 27 | just one if the window is small enough 28 | 29 | - Decide on re-entrancy, keep things from getting tangled up. 30 | - Combine pointinfo and stats panel, and add more info to it. 31 | - Maybe a better way to manage all the little windows? 32 | - Finish basic Julia: 33 | + saving and reading state 34 | + larger Julia exploration window 35 | + don't let J do anything in a Julia window 36 | + YouAreHere is centered incorrectly for a Julia set. 37 | - Explorer, spawn a Julia, shift-hover in the Julia explorer window changes the mini julia. 38 | 39 | - Command line doesn't need to init the pixels with checkerboard, it just consumes extra memory. 40 | 41 | 42 | * Clean-up 43 | 44 | + Use PyMem_Malloc in the extension. 45 | 46 | * Bugs 47 | 48 | x Mac: fullscreen does nothing. 49 | x Mac: the gray checkerboard is randomly tinted other colors. (it was numpy 1.0.1's fault) 50 | - Ubuntu virtualbox: nothing is drawn until the whole calculation is finished. 51 | - Ubuntu virtualbox: keystrokes work at first, and then stop working. 52 | - Ubuntu virtualbox: resizing the window causes many paints (and therefore calcs). 53 | - Somehow I managed to zoom in then zoom out, and ended up outside the legal 54 | radius and the set disappeared. 55 | - Resizing a window larger, the background shows pixels left over from other windows? 56 | 57 | + Stats panel show zero cycles if any of the tiles had zero cycles. 58 | + Visit the deep tush (narrow horizontal splinter). Drag the image to the right. 59 | Tiles that should be all black that still have old pixels in them won't 60 | flood-fill, they paint every scanline. 61 | + Stats panel can show MAX_INT for min cycles if no cycles 62 | + If client size is queried while minimized, it comes back as zero, and a divide by zero error happens. 63 | + Proper prompting about overwriting existing files. 64 | + If you zoom out too far, boundary tracing makes the whole set disappear. 65 | + Progress can decide there's 0 sec left, and shows an empty string. 66 | + When dragging the set, chaotic pixels still onscreen shimmer, prob something 67 | wrong with the half-pixel offset. 68 | + If the file-save picker says "PNG", and the filename is "foo.aptus", it should 69 | save as a .aptus file. 70 | + In continuous mode, there's a stark ring of radius 8 or so. 71 | + Dragging in the main window shouldn't change the position in the Julia window. 72 | + When computing only part of the screen, the percentage complete is wrong b/c 73 | it thinks we have to recompute all the pixels. 74 | + Dragging the window confuses the boundary tracer now that edgemin is in. 75 | se: slow; ne: slow; sw: fast; nw: slow 76 | + Display the YouAreHere panel. Zoom way in. Zoom way out again, the panel doesn't close 77 | automatically when the view window closes, and looks like it doesn't properly 78 | clean up unneeded zoom levels in the stack either. 79 | 80 | * Speed 81 | 82 | + Multi-threaded to use more than one core. 83 | - Faster epsilon comparison for cycle checking (use tricky int operations...) 84 | - http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm 85 | - http://lomont.org/Math/Papers/2005/CompareFloat.pdf 86 | - Take some more optimizations from Xaos: http://www.delorie.com/gnu/docs/xaos/xaos_29.html 87 | - Do iterations in batches of 8 to avoid the bailout comparison. 88 | + Take advantage of symmetry across the x-axis. 89 | - Adaptively choose a number of continuous levels (other than 256) based on the 90 | actual steps in the palette, etc. 91 | - When dragging the image, keep parts scrolled off-screen, in case they come back. 92 | - What does edge-min do to cycle checks? It prevents us from finding shorter cycles, 93 | but does that matter? 94 | x Optimize boundary.py so we can use it for high iterations 95 | + Boundary checking in C 96 | + Fracint computes the miniter by running the edges (where the min must be), then 97 | uses it to skip the bailout check until reaching the miniter. 98 | + Go through the bitblting python code to see if we can skip some of the steps. 99 | + A custom coloring endpoint in the engine to replace the multiple numpy operations. 100 | + When dragging the image, only re-calc the parts that have been exposed. 101 | + When resizing the window, do similar pixel-retention as when dragging. 102 | 103 | * UI 104 | 105 | - Use CaptureMouse on dragging to avoid lost drags. 106 | - Status bar for progress and stats presentation. 107 | - GUI progress indicator. 108 | - Palette editor. 109 | - Back button 110 | - with a list of places you've been (with thumbnails). 111 | + Open command to load aptus state files. 112 | - and palette files 113 | - Mac: app icon (if 'wxMac' in wx.PlatformInfo:) 114 | - Custom bookmarks (like the J key). 115 | - Ubuntu: resizing the window tries to recalc as you drag (bad idle handling?) 116 | - Arrow keys for nudging the viewport. 117 | + Make a window icon. 118 | + Help 119 | + Draggable rectangle in addition to click-to-zoom. 120 | + Multiple windows. 121 | + Paint partial progress in the window. 122 | 123 | * Beauty 124 | 125 | - Other mappings of count to palette: log, for example. 126 | - Generalized palette specification 127 | - Xaos palette options: 128 | - Algorithmic palette 129 | - Add an extra segment to non-cyclic ggr palettes. 130 | - Mirroring non-cyclic palettes. 131 | - Read other palette files: 132 | - .map (see Gnofract 4D) 133 | - .cs (see Gnofract 4D) 134 | - .ugr (see Gnofract 4D) 135 | - More varied palettes. 136 | - Use GEGL to do powerful image stuff? http://gegl.org/ 137 | + HSV palette 138 | + Hue tweaking: [ and ] adjust the hue throughout the current palette. 139 | + Rotation (simply store the pixel offsets as dx and dy computed from angle). 140 | + Continuous coloring (http://linas.org/art-gallery/escape/smooth.html) 141 | + A palette specification, so the complete specs for a pic can be stored. 142 | + Try to reduce flicker during palette changing (http://wiki.wxpython.org/index.cgi/DoubleBufferedDrawing) 143 | When cycling colors, display flashes. (wxWindow::IsDoubleBuffered) 144 | + Reading GIMP gradients as palettes 145 | + Palette shifting 146 | 147 | * Power 148 | 149 | - Editable parameters for things like cycle checking epsilon. 150 | - Multi-precision floats 151 | - Interruptable rendering: write partial counts (and all other state) to a file and restart. 152 | - Save counts in a file so a long compute can then be re-colored easily. 153 | + Refactor engine.c so that it defines a class, rather than having all those globals. 154 | + Command line invocation (refactor a bunch of stuff) 155 | 156 | * Knowledge 157 | 158 | - Stats: what's the census of iteration counts in the image? 159 | - Switchable coloring: standard, cycle count, etc. 160 | + Stats: some counters (totaliter) need to be more than 32-bit ints. 161 | + Better progress reporting for trace_boundary: has to count pixels rather than scanlines. 162 | + Some way of embedding the mandelbrot parameters into the final .PNG, so it can be recreated. 163 | + A You-Are-Here mini panel showing the general location you are viewing 164 | + (maybe more than one step's worth). 165 | + A point-info panel: current position, iteration count, color, palette entry, etc. 166 | 167 | * Safety 168 | 169 | - A warning when zooming in so far that the precision fails. 170 | - A warning about too many maxedpoints (iter_limit too low). 171 | 172 | * Convenience 173 | 174 | - Use ez_setup, or eggs, or something to manage the Python dependencies. 175 | 176 | * MVC Model 177 | - main mandelbrot 178 | - center, rotation 179 | - diameter 180 | - size 181 | - iterlimit, bailout 182 | - continuous 183 | - warnings: iter-too-low, precision underflow 184 | - palette 185 | - colors 186 | - offset 187 | - density 188 | - undo stack: 189 | - parameters 190 | - thumbnail 191 | - you-are-here 192 | 193 | * Done 194 | 195 | + Some kind of progress indicator. 196 | + Cycle detection 197 | 198 | The Beauty of Fractals parameters for Fractint v18: http://groups.google.com/group/sci.fractals/browse_thread/thread/e7873432d97aff8/6299651657de38bc?lnk=gst&q=julia+peitgen#6299651657de38bc 199 | -------------------------------------------------------------------------------- /src/aptus/gui/youarehere.py: -------------------------------------------------------------------------------- 1 | """ YouAreHere stuff for Aptus. 2 | """ 3 | 4 | import math 5 | 6 | import wx 7 | from wx.lib.scrolledpanel import ScrolledPanel 8 | 9 | from aptus import settings 10 | from aptus.gui.computepanel import ComputePanel 11 | from aptus.gui.ids import * 12 | from aptus.gui.misc import AptusToolFrame, ListeningWindowMixin 13 | 14 | 15 | MIN_RECT = 20 16 | ParentComputePanel = ComputePanel 17 | 18 | class YouAreHereWin(ParentComputePanel, ListeningWindowMixin): 19 | """ A panel slaved to another ComputePanel to show where the master panel is 20 | on the Set. These are designed to be stacked in a YouAreHereStack to 21 | show successive magnifications. 22 | 23 | Two windows are referenced: the main view window (so that we can change 24 | the view), and the window our rectangle represents. This can be either 25 | the next YouAreHereWin in the stack, or the main view window in the case 26 | of the last window in the stack. 27 | """ 28 | def __init__(self, parent, mainwin, center, diam, size=wx.DefaultSize): 29 | ParentComputePanel.__init__(self, parent, size=size) 30 | ListeningWindowMixin.__init__(self) 31 | 32 | self.mainwin = mainwin 33 | self.hererect = None 34 | self.diam = diam 35 | 36 | self.Bind(wx.EVT_SIZE, self.on_size) 37 | self.Bind(wx.EVT_IDLE, self.on_idle) 38 | self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) 39 | self.Bind(wx.EVT_LEFT_UP, self.on_left_up) 40 | self.Bind(wx.EVT_MOTION, self.on_motion) 41 | 42 | self.register_listener(self.on_coloring_changed, EVT_APTUS_COLORING_CHANGED, self.mainwin) 43 | self.register_listener(self.on_computation_changed, EVT_APTUS_COMPUTATION_CHANGED, self.mainwin) 44 | 45 | self.set_ref_window(mainwin) 46 | 47 | self.set_geometry(center=center, diam=diam) 48 | self.on_coloring_changed(None) 49 | self.on_computation_changed(None) 50 | self.on_geometry_changed(None) 51 | 52 | self.dragging = False 53 | self.drag_pt = None 54 | 55 | def set_ref_window(self, refwin): 56 | """ Set the other window that our rectangle models. 57 | """ 58 | # Deregister the old geometry listener 59 | self.deregister_listener(self.on_geometry_changed) 60 | 61 | self.rectwin = refwin 62 | 63 | # Register the new listener and calc the rectangle. 64 | self.register_listener(self.on_geometry_changed, EVT_APTUS_GEOMETRY_CHANGED, self.rectwin) 65 | self.calc_rectangle() 66 | 67 | def on_size(self, event): 68 | # Need to recalc our rectangle. 69 | self.hererect = None 70 | ParentComputePanel.on_size(self, event) 71 | 72 | def on_idle(self, event): 73 | # Let the ComputePanel resize. 74 | ParentComputePanel.on_idle(self, event) 75 | # Then we can recalc our rectangle. 76 | if not self.hererect: 77 | self.calc_rectangle() 78 | 79 | def on_left_down(self, event): 80 | mouse_pt = event.GetPosition() 81 | if self.hererect.Contains(mouse_pt): 82 | self.dragging = True 83 | self.drag_pt = mouse_pt 84 | 85 | def on_left_up(self, event): 86 | # Reposition the main window. 87 | if self.dragging: 88 | if self.mainwin == self.rectwin: 89 | # We already show the actual view, so just recenter on the center 90 | # of the rectangle. 91 | mx = self.hererect.x + self.hererect.width/2 92 | my = self.hererect.y + self.hererect.height/2 93 | self.mainwin.set_geometry(center=self.compute.coords_from_pixel(mx, my)) 94 | else: 95 | # Dragging the rect: set the view to include the four corners of 96 | # the rectangle. 97 | ulr, uli = self.compute.coords_from_pixel(*self.hererect.TopLeft) 98 | lrr, lri = self.compute.coords_from_pixel(*self.hererect.BottomRight) 99 | self.mainwin.set_geometry(corners=(ulr, uli, lrr, lri)) 100 | self.dragging = False 101 | else: 102 | # Clicking outside the rect: recenter there. 103 | mx, my = event.GetPosition() 104 | self.mainwin.set_geometry(center=self.compute.coords_from_pixel(mx, my), diam=self.diam) 105 | 106 | def on_motion(self, event): 107 | self.set_cursor(event) 108 | if self.dragging: 109 | mouse_pt = event.GetPosition() 110 | self.hererect.Offset((mouse_pt.x - self.drag_pt.x, mouse_pt.y - self.drag_pt.y)) 111 | self.drag_pt = mouse_pt 112 | self.Refresh() 113 | 114 | def set_cursor(self, event): 115 | # Set the proper cursor: 116 | mouse_pt = event.GetPosition() 117 | if self.dragging or (self.hererect and self.hererect.Contains(mouse_pt)): 118 | self.SetCursor(wx.Cursor(wx.CURSOR_SIZING)) 119 | else: 120 | self.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) 121 | 122 | def on_coloring_changed(self, event_unused): 123 | if self.compute.copy_coloring(self.mainwin.compute): 124 | self.coloring_changed() 125 | 126 | def on_computation_changed(self, event_unused): 127 | if self.compute.copy_computation(self.mainwin.compute): 128 | self.computation_changed() 129 | 130 | def on_geometry_changed(self, event_unused): 131 | # When a geometry_changed event comes in, copy the pertinent info from 132 | # the master window, then compute the window visible in our coordinates 133 | if self.compute.angle != self.mainwin.compute.angle: 134 | self.compute.angle = self.mainwin.compute.angle 135 | self.geometry_changed() 136 | self.calc_rectangle() 137 | 138 | def calc_rectangle(self): 139 | # Compute the master rectangle in our coords. 140 | ulx, uly = self.compute.pixel_from_coords(*self.rectwin.compute.coords_from_pixel(0,0)) 141 | lrx, lry = self.compute.pixel_from_coords(*self.rectwin.compute.coords_from_pixel(*self.rectwin.compute.size)) 142 | ulx = int(math.floor(ulx)) 143 | uly = int(math.floor(uly)) 144 | lrx = int(math.ceil(lrx))+1 145 | lry = int(math.ceil(lry))+1 146 | w, h = lrx-ulx, lry-uly 147 | # Never draw the box smaller than 3 pixels 148 | if w < 3: 149 | w = 3 150 | ulx -= 1 # Scooch back to adjust to the wider window. 151 | if h < 3: 152 | h = 3 153 | uly -= 1 154 | self.hererect = wx.Rect(ulx, uly, w, h) 155 | self.Refresh() 156 | 157 | def on_paint_extras(self, dc): 158 | # Draw the mainwin view window. 159 | if self.hererect: 160 | dc.SetBrush(wx.TRANSPARENT_BRUSH) 161 | dc.SetPen(wx.Pen(wx.Colour(255,255,255), 1, wx.SOLID)) 162 | dc.DrawRectangle(*self.hererect) 163 | 164 | 165 | class YouAreHereStack(ScrolledPanel, ListeningWindowMixin): 166 | """ A scrolled panel with a stack of YouAreHereWin's, each at a successive 167 | magnification. 168 | """ 169 | def __init__(self, parent, viewwin, size=wx.DefaultSize): 170 | ScrolledPanel.__init__(self, parent, size=size) 171 | ListeningWindowMixin.__init__(self) 172 | 173 | self.winsize = 250 174 | self.minrect = MIN_RECT 175 | self.stepfactor = float(self.winsize)/self.minrect 176 | 177 | self.viewwin = viewwin 178 | self.sizer = wx.FlexGridSizer(cols=1, vgap=2, hgap=0) 179 | self.SetSizer(self.sizer) 180 | self.SetAutoLayout(1) 181 | self.SetupScrolling() 182 | 183 | self.register_listener(self.on_geometry_changed, EVT_APTUS_GEOMETRY_CHANGED, self.viewwin) 184 | 185 | self.on_geometry_changed() 186 | 187 | def on_geometry_changed(self, event_unused=None): 188 | mode = self.viewwin.compute.mode 189 | diam = min(settings.diam(mode)) 190 | 191 | # How many YouAreHereWin's will we need? 192 | targetdiam = min(self.viewwin.compute.diam) 193 | num_wins = int(math.ceil((math.log(diam)-math.log(targetdiam))/math.log(self.stepfactor))) 194 | num_wins = num_wins or 1 195 | 196 | cur_wins = list(self.sizer.Children) 197 | last = None 198 | for i in range(num_wins): 199 | if i == 0: 200 | # Don't recenter the topmost YouAreHere. 201 | center = settings.center(mode) 202 | else: 203 | center = self.viewwin.compute.center 204 | if i < len(cur_wins): 205 | # Re-using an existing window in the stack. 206 | win = cur_wins[i].Window 207 | win.set_geometry(center=center, diam=(diam,diam)) 208 | else: 209 | # Going deeper: have to make a new window. 210 | win = YouAreHereWin( 211 | self, self.viewwin, center=center, 212 | diam=(diam,diam), size=(self.winsize, self.winsize) 213 | ) 214 | self.sizer.Add(win) 215 | if last: 216 | last.set_ref_window(win) 217 | last = win 218 | diam /= self.stepfactor 219 | 220 | # The last window needs to draw a rectangle for the view window. 221 | last.set_ref_window(self.viewwin) 222 | 223 | # Remove windows we no longer need. 224 | if 0: 225 | for child in cur_wins[num_wins:]: 226 | self.sizer.Remove(child.Window) 227 | child.Window.Destroy() 228 | 229 | for i in reversed(range(num_wins, len(cur_wins))): 230 | print("Thing to delete:", cur_wins[i]) 231 | print("the window:", cur_wins[i].Window) 232 | win = cur_wins[i].Window 233 | self.sizer.Remove(i) 234 | win.Destroy() 235 | 236 | self.sizer.Layout() 237 | self.SetupScrolling() 238 | 239 | 240 | class YouAreHereFrame(AptusToolFrame): 241 | def __init__(self, mainframe, viewwin): 242 | AptusToolFrame.__init__(self, mainframe, title='You are here', size=(250,550)) 243 | self.stack = YouAreHereStack(self, viewwin) 244 | -------------------------------------------------------------------------------- /src/aptus/gui/mainframe.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | import re 4 | 5 | import wx 6 | import wx.aui 7 | 8 | from aptus import data_file 9 | from aptus.gui.ids import * 10 | from aptus.gui.viewpanel import AptusViewPanel 11 | from aptus.gui.misc import AptusToolableFrameMixin 12 | from aptus.options import AptusOptions 13 | 14 | 15 | class AptusMainFrame(wx.Frame, AptusToolableFrameMixin): 16 | """ The main window frame of the Aptus app. 17 | """ 18 | def __init__(self, args=None, compute=None, size=None): 19 | """ Create an Aptus main GUI frame. `args` is an argv-style list of 20 | command-line arguments. `compute` is an existing compute object to 21 | copy settings from. 22 | """ 23 | wx.Frame.__init__(self, None, -1, 'Aptus') 24 | AptusToolableFrameMixin.__init__(self) 25 | 26 | # Make the panel 27 | self.panel = AptusViewPanel(self) 28 | 29 | if args: 30 | opts = AptusOptions(self.panel.compute) 31 | opts.read_args(args) 32 | if compute: 33 | self.panel.compute.copy_all(compute) 34 | 35 | if size: 36 | self.panel.compute.size = size 37 | 38 | self.panel.compute.supersample = 1 39 | 40 | if 0: 41 | # Experimental AUI support 42 | self.auimgr = wx.aui.AuiManager() 43 | self.auimgr.SetManagedWindow(self) 44 | 45 | self.auimgr.AddPane(self.panel, wx.aui.AuiPaneInfo().Name("grid_content"). 46 | PaneBorder(False).CenterPane()) 47 | 48 | from aptus.gui import pointinfo 49 | self.pointinfo_tool = pointinfo.PointInfoPanel(self, self.panel) 50 | 51 | self.auimgr.AddPane(self.pointinfo_tool, wx.aui.AuiPaneInfo(). 52 | Name("pointinfo").Caption("Point info"). 53 | Right().Layer(1).Position(1).CloseButton(True)) 54 | 55 | self.auimgr.Update() 56 | 57 | # Set the window icon 58 | ib = wx.IconBundle() 59 | ib.AddIcon(data_file("icon48.png"), wx.BITMAP_TYPE_ANY) 60 | ib.AddIcon(data_file("icon32.png"), wx.BITMAP_TYPE_ANY) 61 | ib.AddIcon(data_file("icon16.png"), wx.BITMAP_TYPE_ANY) 62 | self.SetIcons(ib) 63 | 64 | # Bind commands 65 | self.Bind(wx.EVT_MENU, self.cmd_new, id=id_new) 66 | self.Bind(wx.EVT_MENU, self.cmd_save, id=id_save) 67 | self.Bind(wx.EVT_MENU, self.cmd_open, id=id_open) 68 | self.Bind(wx.EVT_MENU, self.cmd_help, id=id_help) 69 | self.Bind(wx.EVT_MENU, self.cmd_fullscreen, id=id_fullscreen) 70 | self.Bind(wx.EVT_MENU, self.cmd_window_size, id=id_window_size) 71 | self.Bind(wx.EVT_MENU, self.cmd_show_youarehere, id=id_show_youarehere) 72 | self.Bind(wx.EVT_MENU, self.cmd_show_palettes, id=id_show_palettes) 73 | self.Bind(wx.EVT_MENU, self.cmd_show_stats, id=id_show_stats) 74 | self.Bind(wx.EVT_MENU, self.cmd_show_pointinfo, id=id_show_pointinfo) 75 | self.Bind(wx.EVT_MENU, self.cmd_show_julia, id=id_show_julia) 76 | 77 | # Auxilliary frames. 78 | self.youarehere_tool = None 79 | self.palettes_tool = None 80 | self.stats_tool = None 81 | self.pointinfo_tool = None 82 | self.julia_tool = None 83 | 84 | # Files can be dropped here. 85 | self.SetDropTarget(MainFrameFileDropTarget(self)) 86 | 87 | def Show(self, show=True): 88 | # Override Show so we can set the view properly. 89 | if show: 90 | self.SetClientSize(self.panel.compute.size) 91 | self.panel.set_view() 92 | wx.Frame.Show(self, True) 93 | self.panel.SetFocus() 94 | else: 95 | wx.Frame.Show(self, False) 96 | 97 | def message(self, msg): 98 | dlg = wx.MessageDialog(self, msg, 'Aptus', wx.OK | wx.ICON_WARNING) 99 | dlg.ShowModal() 100 | dlg.Destroy() 101 | 102 | # Command handlers. 103 | 104 | def show_file_dialog(self, dlg): 105 | """ Show a file dialog, and do some post-processing on the result. 106 | Returns a pair: type, path. 107 | Type is one of the extensions from the wildcard choices. 108 | """ 109 | if dlg.ShowModal() == wx.ID_OK: 110 | pth = dlg.Path 111 | ext = os.path.splitext(pth)[1].lower() 112 | idx = dlg.FilterIndex 113 | wildcards = dlg.Wildcard.split('|') 114 | wildcard = wildcards[2*idx+1] 115 | if wildcard == '*.*': 116 | if ext: 117 | typ = ext[1:] 118 | else: 119 | typ = '' 120 | elif '*'+ext in wildcards: 121 | # The extension of the file is a recognized extension: 122 | # Use it regardless of the file type chosen in the picker. 123 | typ = ext[1:] 124 | else: 125 | typ = wildcard.split('.')[-1].lower() 126 | if ext == '' and typ != '': 127 | pth += '.' + typ 128 | return typ, pth 129 | else: 130 | return None, None 131 | 132 | def cmd_new(self, event_unused): 133 | return wx.GetApp().new_window() 134 | 135 | # Files we can open and save. 136 | wildcards = ( 137 | "PNG image (*.png)|*.png|" 138 | "Aptus state (*.aptus)|*.aptus|" 139 | "All files (*.*)|*.*" 140 | ) 141 | 142 | def cmd_save(self, event_unused): 143 | dlg = wx.FileDialog( 144 | self, message="Save", defaultDir=os.getcwd(), defaultFile="", 145 | style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, wildcard=self.wildcards 146 | ) 147 | 148 | typ, pth = self.show_file_dialog(dlg) 149 | if typ: 150 | if typ == 'png': 151 | self.panel.write_png(pth) 152 | elif typ == 'aptus': 153 | self.panel.write_aptus(pth) 154 | else: 155 | self.message("Don't understand how to write file '%s'" % pth) 156 | 157 | def cmd_open(self, event_unused): 158 | dlg = wx.FileDialog( 159 | self, message="Open", defaultDir=os.getcwd(), defaultFile="", 160 | style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST, wildcard=self.wildcards 161 | ) 162 | typ, pth = self.show_file_dialog(dlg) 163 | if typ: 164 | self.open_file(pth) 165 | 166 | def open_file(self, pth): 167 | opts = AptusOptions(self.panel.compute) 168 | opts.opts_from_file(pth) 169 | self.SetClientSize(self.panel.compute.size) 170 | self.panel.fire_command(id_redraw) 171 | 172 | def cmd_help(self, event_unused): 173 | from aptus.gui.help import HelpDlg 174 | dlg = HelpDlg(self) 175 | dlg.ShowModal() 176 | 177 | def cmd_fullscreen(self, event_unused): 178 | self.ShowFullScreen(not self.IsFullScreen()) 179 | 180 | def cmd_window_size(self, event_unused): 181 | cur_size = "%d x %d" % tuple(self.GetClientSize()) 182 | dlg = wx.TextEntryDialog(self.GetTopLevelParent(), "Window size", 183 | "New window size?", cur_size) 184 | 185 | if dlg.ShowModal() == wx.ID_OK: 186 | new_size = dlg.GetValue().strip() 187 | m = re.match(r"(?P\d+)\s*[x, ]\s*(?P\d+)|s/(?P[\d.]+)", new_size) 188 | if m: 189 | if m.group('mini') is not None: 190 | factor = float(m.group('mini')) 191 | screen_w, screen_h = wx.GetDisplaySize() 192 | w, h = screen_w/factor, screen_h/factor 193 | elif m.group('w') is not None: 194 | w, h = int(m.group('w')), int(m.group('h')) 195 | self.SetClientSize((w,h)) 196 | dlg.Destroy() 197 | 198 | def cmd_show_youarehere(self, event_unused): 199 | """ Toggle the presence of the YouAreHere tool. 200 | """ 201 | if self.youarehere_tool: 202 | self.youarehere_tool.Destroy() 203 | else: 204 | from aptus.gui import youarehere 205 | self.youarehere_tool = youarehere.YouAreHereFrame(self, self.panel) 206 | self.youarehere_tool.Show() 207 | 208 | def cmd_show_palettes(self, event_unused): 209 | """ Toggle the presence of the Palettes tool. 210 | """ 211 | if self.palettes_tool: 212 | self.palettes_tool.Destroy() 213 | else: 214 | from aptus.gui import palettespanel 215 | from aptus.palettes import all_palettes 216 | self.palettes_tool = palettespanel.PalettesFrame(self, all_palettes, self.panel) 217 | self.palettes_tool.Show() 218 | 219 | def cmd_show_stats(self, event_unused): 220 | """ Toggle the presence of the Stats tool. 221 | """ 222 | if self.stats_tool: 223 | self.stats_tool.Destroy() 224 | else: 225 | from aptus.gui import statspanel 226 | self.stats_tool = statspanel.StatsFrame(self, self.panel) 227 | self.stats_tool.Show() 228 | 229 | def cmd_show_pointinfo(self, event_unused): 230 | """ Toggle the presence of the PointInfo tool. 231 | """ 232 | if self.pointinfo_tool: 233 | self.pointinfo_tool.Destroy() 234 | else: 235 | from aptus.gui import pointinfo 236 | self.pointinfo_tool = pointinfo.PointInfoFrame(self, self.panel) 237 | self.pointinfo_tool.Show() 238 | 239 | def cmd_show_julia(self, event_unused): 240 | """ Toggle the presence of the Julia tool. 241 | """ 242 | if self.panel.compute.mode == 'mandelbrot': 243 | if self.julia_tool: 244 | self.julia_tool.Destroy() 245 | else: 246 | from aptus.gui import juliapanel 247 | self.julia_tool = juliapanel.JuliaFrame(self, self.panel) 248 | self.julia_tool.Show() 249 | 250 | 251 | class MainFrameFileDropTarget(wx.FileDropTarget): 252 | """A drop target so files can be opened by dragging them to the Aptus window. 253 | 254 | The first file opens in the current window, the rest open new windows. 255 | 256 | """ 257 | def __init__(self, frame): 258 | wx.FileDropTarget.__init__(self) 259 | self.frame = frame 260 | 261 | def OnDropFiles(self, x, y, filenames): 262 | self.frame.open_file(filenames[0]) 263 | for filename in filenames[1:]: 264 | frame = self.frame.cmd_new(None) 265 | frame.open_file(filename) 266 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # lint Python modules using external checkers. 2 | # 3 | # This is the main checker controling the other ones and the reports 4 | # generation. It is itself both a raw checker and an astng checker in order 5 | # to: 6 | # * handle message activation / deactivation at the module level 7 | # * handle some basic but necessary stats'data (number of classes, methods...) 8 | # 9 | [MASTER] 10 | 11 | # Specify a configuration file. 12 | #rcfile= 13 | 14 | # Python code to execute, usually for sys.path manipulation such as 15 | # pygtk.require(). 16 | #init-hook= 17 | 18 | # Profiled execution. 19 | profile=no 20 | 21 | # Add to the black list. It should be a base name, not a 22 | # path. You may set this option multiple times. 23 | ignore=CVS 24 | 25 | # Pickle collected data for later comparisons. 26 | persistent=yes 27 | 28 | # Set the cache size for astng objects. 29 | cache-size=500 30 | 31 | # List of plugins (as comma separated values of python modules names) to load, 32 | # usually to register additional checkers. 33 | load-plugins= 34 | 35 | 36 | [MESSAGES CONTROL] 37 | 38 | # Enable only checker(s) with the given id(s). This option conflicts with the 39 | # disable-checker option 40 | #enable-checker= 41 | 42 | # Enable all checker(s) except those with the given id(s). This option 43 | # conflicts with the enable-checker option 44 | #disable-checker= 45 | 46 | # Enable all messages in the listed categories. 47 | #enable-msg-cat= 48 | 49 | # Disable all messages in the listed categories. 50 | #disable-msg-cat= 51 | 52 | # Enable the message(s) with the given id(s). 53 | #enable-msg= 54 | 55 | # Disable the message(s) with the given id(s). 56 | # I0011:106: Locally disabling E1101 57 | # C0103: 10: Invalid name "wx" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) 58 | # C0111: 12:AptusGuiApp: Missing docstring 59 | # C0324:205:YouAreHereStack.on_geometry_changed: Comma not followed by a space 60 | # R0201: 35:GuiProgressReporter.end: Method could be a function 61 | # R0801: 1: Similar lines in 2 files 62 | # R0901:164:YouAreHereStack: Too many ancestors (9/7) 63 | # R0903: 17:GimpGradient._segment: Too few public methods (0/1) 64 | # W0142: 49:PaletteWin.on_paint: Used * or ** magic 65 | # W0201: 84:ComputePanel.on_size: Attribute 'check_size' defined outside __init__ 66 | # W0232: 64:JsonWriter: Class has no __init__ method 67 | # W0401: 7: Wildcard import aptus.gui.ids 68 | # W0603: 94:ConsoleProgressReporter.end: Using the global statement 69 | # W0614: 6: Unused import id_show_palettes from wildcard import 70 | disable-msg=I0011,C0103,C0111,C0324, R0201,R0801,R0901,R0903, W0142,W0201,W0232,W0401,W0603,W0614 71 | 72 | [REPORTS] 73 | 74 | # set the output format. Available formats are text, parseable, colorized, msvs 75 | # (visual studio) and html 76 | output-format=text 77 | 78 | # Include message's id in output 79 | include-ids=yes 80 | 81 | # Put messages in a separate file for each module / package specified on the 82 | # command line instead of printing them on stdout. Reports (if any) will be 83 | # written in a file name "pylint_global.[txt|html]". 84 | files-output=no 85 | 86 | # Tells wether to display a full report or only the messages 87 | reports=no 88 | 89 | # Python expression which should return a note less than 10 (10 is the highest 90 | # note).You have access to the variables errors warning, statement which 91 | # respectivly contain the number of errors / warnings messages and the total 92 | # number of statements analyzed. This is used by the global evaluation report 93 | # (R0004). 94 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 95 | 96 | # Add a comment according to your evaluation note. This is used by the global 97 | # evaluation report (R0004). 98 | comment=no 99 | 100 | # Enable the report(s) with the given id(s). 101 | #enable-report= 102 | 103 | # Disable the report(s) with the given id(s). 104 | #disable-report= 105 | 106 | 107 | # checks for : 108 | # * doc strings 109 | # * modules / classes / functions / methods / arguments / variables name 110 | # * number of arguments, local variables, branchs, returns and statements in 111 | # functions, methods 112 | # * required module attributes 113 | # * dangerous default values as arguments 114 | # * redefinition of function / method / class 115 | # * uses of the global statement 116 | # 117 | [BASIC] 118 | 119 | # Required attributes for module, separated by a comma 120 | required-attributes= 121 | 122 | # Regular expression which should only match functions or classes name which do 123 | # not require a docstring 124 | no-docstring-rgx=__.*__ 125 | 126 | # Regular expression which should only match correct module names 127 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 128 | 129 | # Regular expression which should only match correct module level names 130 | const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ 131 | 132 | # Regular expression which should only match correct class names 133 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 134 | 135 | # Regular expression which should only match correct function names 136 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 137 | 138 | # Regular expression which should only match correct method names 139 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 140 | 141 | # Regular expression which should only match correct instance attribute names 142 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 143 | 144 | # Regular expression which should only match correct argument names 145 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 146 | 147 | # Regular expression which should only match correct variable names 148 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 149 | 150 | # Regular expression which should only match correct list comprehension / 151 | # generator expression variable names 152 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 153 | 154 | # Good variable names which should always be accepted, separated by a comma 155 | good-names=i,j,k,ex,Run,_ 156 | 157 | # Bad variable names which should always be refused, separated by a comma 158 | bad-names=foo,bar,baz,toto,tutu,tata 159 | 160 | # List of builtins function names that should not be used, separated by a comma 161 | bad-functions= 162 | 163 | 164 | # try to find bugs in the code using type inference 165 | # 166 | [TYPECHECK] 167 | 168 | # Tells wether missing members accessed in mixin class should be ignored. A 169 | # mixin class is detected if its name ends with "mixin" (case insensitive). 170 | ignore-mixin-members=yes 171 | 172 | # List of classes names for which member attributes should not be checked 173 | # (useful for classes with attributes dynamicaly set). 174 | ignored-classes=SQLObject 175 | 176 | # When zope mode is activated, consider the acquired-members option to ignore 177 | # access to some undefined attributes. 178 | zope=no 179 | 180 | # List of members which are usually get through zope's acquisition mecanism and 181 | # so shouldn't trigger E0201 when accessed (need zope=yes to be considered). 182 | acquired-members=REQUEST,acl_users,aq_parent 183 | 184 | 185 | # checks for 186 | # * unused variables / imports 187 | # * undefined variables 188 | # * redefinition of variable from builtins or from an outer scope 189 | # * use of variable before assigment 190 | # 191 | [VARIABLES] 192 | 193 | # Tells wether we should check for unused import in __init__ files. 194 | init-import=no 195 | 196 | # A regular expression matching names used for dummy variables (i.e. not used). 197 | dummy-variables-rgx=_|dummy|.*_unused 198 | 199 | # List of additional names supposed to be defined in builtins. Remember that 200 | # you should avoid to define new builtins when possible. 201 | additional-builtins= 202 | 203 | 204 | # checks for : 205 | # * methods without self as first argument 206 | # * overridden methods signature 207 | # * access only to existant members via self 208 | # * attributes not defined in the __init__ method 209 | # * supported interfaces implementation 210 | # * unreachable code 211 | # 212 | [CLASSES] 213 | 214 | # List of interface methods to ignore, separated by a comma. This is used for 215 | # instance to not check methods defines in Zope's Interface base class. 216 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 217 | 218 | # List of method names used to declare (i.e. assign) instance attributes. 219 | defining-attr-methods=__init__,__new__,setUp 220 | 221 | 222 | # checks for sign of poor/misdesign: 223 | # * number of methods, attributes, local variables... 224 | # * size, complexity of functions, methods 225 | # 226 | [DESIGN] 227 | 228 | # Maximum number of arguments for function / method 229 | max-args=15 230 | 231 | # Maximum number of locals for function / method body 232 | max-locals=50 233 | 234 | # Maximum number of return / yield for function / method body 235 | max-returns=20 236 | 237 | # Maximum number of branch for function / method body 238 | max-branchs=50 239 | 240 | # Maximum number of statements in function / method body 241 | max-statements=150 242 | 243 | # Maximum number of parents for a class (see R0901). 244 | max-parents=7 245 | 246 | # Maximum number of attributes for a class (see R0902). 247 | max-attributes=40 248 | 249 | # Minimum number of public methods for a class (see R0903). 250 | min-public-methods=1 251 | 252 | # Maximum number of public methods for a class (see R0904). 253 | max-public-methods=500 254 | 255 | 256 | # checks for 257 | # * external modules dependencies 258 | # * relative / wildcard imports 259 | # * cyclic imports 260 | # * uses of deprecated modules 261 | # 262 | [IMPORTS] 263 | 264 | # Deprecated modules which should not be used, separated by a comma 265 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 266 | 267 | # Create a graph of every (i.e. internal and external) dependencies in the 268 | # given file (report R0402 must not be disabled) 269 | import-graph= 270 | 271 | # Create a graph of external dependencies in the given file (report R0402 must 272 | # not be disabled) 273 | ext-import-graph= 274 | 275 | # Create a graph of internal dependencies in the given file (report R0402 must 276 | # not be disabled) 277 | int-import-graph= 278 | 279 | 280 | # checks for : 281 | # * unauthorized constructions 282 | # * strict indentation 283 | # * line length 284 | # * use of <> instead of != 285 | # 286 | [FORMAT] 287 | 288 | # Maximum number of characters on a single line. 289 | max-line-length=120 290 | 291 | # Maximum number of lines in a module 292 | max-module-lines=1000 293 | 294 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 295 | # tab). 296 | indent-string=' ' 297 | 298 | 299 | # checks for: 300 | # * warning notes in the code like FIXME, XXX 301 | # * PEP 263: source code with non ascii character but no encoding declaration 302 | # 303 | [MISCELLANEOUS] 304 | 305 | # List of note tags to take in consideration, separated by a comma. 306 | notes=FIXME,XXX,TODO 307 | 308 | 309 | # checks for similarities and duplicated code. This computation may be 310 | # memory / CPU intensive, so you should disable it if you experiments some 311 | # problems. 312 | # 313 | [SIMILARITIES] 314 | 315 | # Minimum lines number of a similarity. 316 | min-similarity-lines=4 317 | 318 | # Ignore comments when computing similarities. 319 | ignore-comments=yes 320 | 321 | # Ignore docstrings when computing similarities. 322 | ignore-docstrings=yes 323 | -------------------------------------------------------------------------------- /src/aptus/palettes.py: -------------------------------------------------------------------------------- 1 | """ Palettes for Aptus. 2 | http://nedbatchelder.com/code/aptus 3 | """ 4 | 5 | import colorsys 6 | import math 7 | 8 | from aptus import data_file 9 | 10 | # Pure data-munging functions 11 | def _255(*vals): 12 | """ Convert all arguments from 0-1.0 to 0-255. 13 | """ 14 | return [int(round(x * 255)) for x in vals] 15 | 16 | def _1(*vals): 17 | """ Convert all arguments from 0-255 to 0-1.0. 18 | """ 19 | return [x/255.0 for x in vals] 20 | 21 | def _clip(val, lo, hi): 22 | """ Clip a val to staying between lo and hi. 23 | """ 24 | if val < lo: 25 | val = lo 26 | if val > hi: 27 | val = hi 28 | return val 29 | 30 | 31 | class Palette: 32 | """ A palette is a list of colors for coloring the successive bands of the 33 | Mandelbrot set. 34 | 35 | colors is a list of RGB triples, 0-255, for display. 36 | fcolors is a list of RGB triples, 0.0-1.0, for computation. 37 | incolor is the RGB255 color for the interior of the set. 38 | _spec is a value that can be passed to from_spec to reconstitute the 39 | palette. It's returned by the spec property. 40 | """ 41 | 42 | default_adjusts = {'hue': 0, 'saturation': 0, 'lightness': 0} 43 | 44 | def __init__(self): 45 | self.incolor = (0,0,0) 46 | 47 | # HSL colors, range 0-1. 48 | self.fcolors = [(0.0,0.0,0.0), (1.0,1.0,1.0)] 49 | # RGB colors, range 0-255. 50 | self.colors = [] 51 | # RGB colors as one bytestring 52 | self._colorbytes = b"" 53 | 54 | self._spec = [] 55 | self.adjusts = dict(self.default_adjusts) 56 | self.wrap = True 57 | 58 | self._colors_from_fcolors() 59 | 60 | def __len__(self): 61 | return len(self.fcolors) 62 | 63 | def __eq__(self, other): 64 | return self.colors == other.colors 65 | 66 | def __ne__(self, other): 67 | return not self.__eq__(other) 68 | 69 | def _colors_from_fcolors(self): 70 | """ Set self.colors from self.fcolors, adjusting them for hue, etc, 71 | in the process. 72 | """ 73 | self.colors = [] 74 | 75 | hue_adj = self.adjusts['hue'] / 360.0 76 | sat_adj = self.adjusts['saturation'] / 255.0 77 | lgt_adj = self.adjusts['lightness'] / 100.0 78 | 79 | for h, l, s in self.fcolors: 80 | h = (h + hue_adj) % 1.0 81 | s = _clip(s + sat_adj, 0.0, 1.0) 82 | if lgt_adj: 83 | if lgt_adj > 0: 84 | l += (1 - l) * lgt_adj 85 | else: 86 | l -= l * -lgt_adj 87 | self.colors.append(_255(*colorsys.hls_to_rgb(h, l, s))) 88 | self._colorbytes = b"" 89 | 90 | def color_bytes(self): 91 | """ Compute a string of RGB bytes for use in the engine. 92 | """ 93 | if not self._colorbytes: 94 | colbytes = b"".join(bytes([r, g, b]) for r,g,b in self.colors) 95 | self._colorbytes = colbytes 96 | return self._colorbytes 97 | 98 | def spec(self): 99 | """ Create a textual description of the palette, for later reconstitution 100 | with from_spec(). 101 | """ 102 | s = self._spec[:] 103 | if self.adjusts != self.default_adjusts: 104 | s.append(['adjust', self.adjusts]) 105 | if self.incolor != (0,0,0): 106 | s.append(['rgb_incolor', {'color': self.incolor}]) 107 | if not self.wrap: 108 | s.append(['wrapping', {'wrap': 0}]) 109 | return s 110 | 111 | def rgb_colors(self, colors): 112 | """ Use an explicit list of RGB 0-255 colors as the palette. 113 | """ 114 | self.colors = colors[:] 115 | self.fcolors = [colorsys.rgb_to_hls(*_1(*rgb255)) for rgb255 in self.colors] 116 | self._colorbytes = None 117 | self._spec.append(['rgb_colors', {'colors':colors}]) 118 | return self 119 | 120 | def spectrum(self, ncolors, h=(0,360), l=(50,200), s=150): 121 | if isinstance(h, (int, float)): 122 | h = (int(h), int(h)) 123 | if isinstance(l, (int, float)): 124 | l = (int(l), int(l)) 125 | if isinstance(s, (int, float)): 126 | s = (int(s), int(s)) 127 | 128 | hlo, hhi = h 129 | llo, lhi = l 130 | slo, shi = s 131 | 132 | self.fcolors = [] 133 | for pt in range(ncolors//2): 134 | hfrac = (pt*1.0/(ncolors/2)) 135 | hue = hlo + (hhi-hlo)*hfrac 136 | self.fcolors.append((hue/360.0, llo/255.0, slo/255.0)) 137 | 138 | hfrac = (pt*1.0+0.5)/(ncolors/2) 139 | hue = hlo + (hhi-hlo)*hfrac 140 | self.fcolors.append((hue/360.0, lhi/255.0, shi/255.0)) 141 | self._colors_from_fcolors() 142 | 143 | args = {'ncolors':ncolors} 144 | if h != (0,360): 145 | if hlo == hhi: 146 | args['h'] = hlo 147 | else: 148 | args['h'] = h 149 | if l != (50,200): 150 | if llo == lhi: 151 | args['l'] = llo 152 | else: 153 | args['l'] = l 154 | if s != (150,150): 155 | if slo == shi: 156 | args['s'] = slo 157 | else: 158 | args['s'] = s 159 | self._spec.append(['spectrum', args]) 160 | return self 161 | 162 | def stretch(self, steps, hsl=False, ease=None): 163 | """ Interpolate between colors in the palette, stretching it out. 164 | Works in either RGB or HSL space. 165 | """ 166 | fcolors = [None]*(len(self.fcolors)*steps) 167 | for i in range(len(fcolors)): 168 | color_index = i//steps 169 | a0, b0, c0 = self.fcolors[color_index] 170 | a1, b1, c1 = self.fcolors[(color_index + 1) % len(self.fcolors)] 171 | if hsl: 172 | if a1 < a0 and a0-a1 > 0.01: 173 | a1 += 1 174 | else: 175 | a0, b0, c0 = colorsys.hls_to_rgb(a0, b0, c0) 176 | a1, b1, c1 = colorsys.hls_to_rgb(a1, b1, c1) 177 | 178 | step = i % steps / steps 179 | 180 | if ease == "sine": 181 | step = -(math.cos(math.pi * step) - 1) / 2; 182 | elif isinstance(ease, (int, float)): 183 | if step < 0.5: 184 | step = math.pow(2 * step, ease) / 2 185 | else: 186 | step = 1 - math.pow(-2 * step + 2, ease) / 2 187 | 188 | ax, bx, cx = ( 189 | a0 + (a1 - a0) * step, 190 | b0 + (b1 - b0) * step, 191 | c0 + (c1 - c0) * step, 192 | ) 193 | if not hsl: 194 | ax, bx, cx = colorsys.rgb_to_hls(ax, bx, cx) 195 | fcolors[i] = (ax, bx, cx) 196 | self.fcolors = fcolors 197 | self._colors_from_fcolors() 198 | self._spec.append(['stretch', {'steps':steps, 'hsl':hsl, 'ease':ease}]) 199 | return self 200 | 201 | def adjust(self, hue=0, saturation=0, lightness=0): 202 | """ Make adjustments to various aspects of the display of the palette. 203 | 0 <= hue <= 360 204 | -255 <= saturation <= 255 205 | -100 <= lightness <= 100 206 | """ 207 | adj = self.adjusts 208 | adj['hue'] = (adj['hue'] + hue) % 360 209 | adj['saturation'] = _clip(adj['saturation'] + saturation, -255, 255) 210 | adj['lightness'] = _clip(adj['lightness'] + lightness, -100, 100) 211 | self._colors_from_fcolors() 212 | return self 213 | 214 | def reset(self): 215 | """ Reset all palette adjustments. 216 | """ 217 | self.adjusts = {'hue': 0, 'saturation': 0, 'lightness': 0} 218 | self._colors_from_fcolors() 219 | return self 220 | 221 | def rgb_incolor(self, color): 222 | """ Set the color for the interior of the Mandelbrot set. 223 | """ 224 | self.incolor = color 225 | return self 226 | 227 | def wrapping(self, wrap): 228 | """ Set the wrap boolean on or off. 229 | """ 230 | self.wrap = wrap 231 | return self 232 | 233 | def gradient(self, ggr_file, ncolors): 234 | """ Create the palette from a GIMP .ggr gradient file. 235 | """ 236 | from aptus.ggr import GimpGradient 237 | ggr = GimpGradient() 238 | try: 239 | ggr.read(ggr_file) 240 | self.fcolors = [ 241 | colorsys.rgb_to_hls(*ggr.color(float(c)/ncolors)) for c in range(ncolors) 242 | ] 243 | except IOError: 244 | self.fcolors = [ (0.0,0.0,0.0), (1.0,0.0,0.0), (1.0,1.0,1.0) ] 245 | self._colors_from_fcolors() 246 | self._spec.append(['gradient', {'ggr_file':ggr_file, 'ncolors':ncolors}]) 247 | return self 248 | 249 | def xaos(self): 250 | # Colors taken from Xaos, to get the same rendering. 251 | xaos_colors = [ 252 | (0, 0, 0), 253 | (120, 119, 238), 254 | (24, 7, 25), 255 | (197, 66, 28), 256 | (29, 18, 11), 257 | (135, 46, 71), 258 | (24, 27, 13), 259 | (241, 230, 128), 260 | (17, 31, 24), 261 | (240, 162, 139), 262 | (11, 4, 30), 263 | (106, 87, 189), 264 | (29, 21, 14), 265 | (12, 140, 118), 266 | (10, 6, 29), 267 | (50, 144, 77), 268 | (22, 0, 24), 269 | (148, 188, 243), 270 | (4, 32, 7), 271 | (231, 146, 14), 272 | (10, 13, 20), 273 | (184, 147, 68), 274 | (13, 28, 3), 275 | (169, 248, 152), 276 | (4, 0, 34), 277 | (62, 83, 48), 278 | (7, 21, 22), 279 | (152, 97, 184), 280 | (8, 3, 12), 281 | (247, 92, 235), 282 | (31, 32, 16) 283 | ] 284 | 285 | self.rgb_colors(xaos_colors) 286 | del self._spec[-1] 287 | self.stretch(8, hsl=False) 288 | del self._spec[-1] 289 | self._spec.append(['xaos', {}]) 290 | return self 291 | 292 | def from_spec(self, spec): 293 | for op, args in spec: 294 | getattr(self, op)(**args) 295 | return self 296 | 297 | all_palettes = [ 298 | Palette().spectrum(12).stretch(10, hsl=True), 299 | Palette().spectrum(12).stretch(10, hsl=True, ease="sine"), 300 | Palette().spectrum(12, l=(50,150), s=150).stretch(25, hsl=True), 301 | Palette().spectrum(12, l=(50,150), s=150).stretch(25, hsl=True, ease="sine"), 302 | Palette().spectrum(64, l=125, s=175), 303 | Palette().spectrum(48, l=(100,150), s=175).stretch(5, hsl=False), 304 | Palette().spectrum(2, h=250, l=(100,150), s=175).stretch(10, hsl=True), 305 | Palette().spectrum(2, h=290, l=(75,175), s=(230,25)).stretch(10, hsl=True), 306 | Palette().spectrum(16, l=125, s=175), 307 | Palette().xaos(), 308 | Palette().spectrum(2, h=120, l=(50,200), s=125).stretch(128, hsl=True), 309 | # US flag: 310 | Palette().rgb_colors([(0x00,0x28,0x68), (0xFF, 0xFF, 0xFF), (0xBF, 0x0A, 0x30), (0xFF, 0xFF, 0xFF)]).stretch(4, ease="sine"), 311 | # Ukraine flag: 312 | Palette().rgb_colors([(0x22,0x5C,0xB5)] * 10 + [(0xF9,0xD5,0x48)]).stretch(4, ease="sine"), 313 | Palette().rgb_colors([(255,255,255), (0,0,0), (0,0,0), (0,0,0)]), 314 | Palette().rgb_colors([(255,255,255)]), 315 | ] 316 | -------------------------------------------------------------------------------- /doc/index.px: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | Created. 6 | Update to 1.5. 7 | Update to 1.51. 8 | Update to 1.55. 9 | Update to 1.56. 10 | Update to 2.0. 11 | Added bitbucket link. 12 | 13 | 14 | 15 | This page is about an outdated version of Aptus. Instead you should read about 16 | the latest version, Aptus v3. 17 | 18 | 19 |

Aptus is a Mandelbrot set viewer and renderer. It is written in Python 20 | with a computation engine in C for speed. 21 |

22 | 23 | 24 | A portion of the Mandelbrot set 25 | 26 | 27 | 28 |

Getting Aptus

29 | 30 |

Pre-requisites

31 |

Aptus installs like any other Python package. 32 | Before installing Aptus, you'll need to install these prerequisite Python packages:

33 | 34 | 39 | 40 |

Installation

41 |

Download the kit that's right for you. Linux and Mac use the tar.gz, Windows 42 | users will probably be happier with the .exe:

43 | 44 | 45 | 46 | 47 |

Install the kit with the usual command:

48 | 49 | python setup.py install 50 | 51 |

Source

52 | 53 |

The source is available on bitbucket if you 54 | prefer direct access to the code, including recent changes.

55 | 56 | 57 |

Using Aptus

58 | 59 |

There are two ways to use Aptus: a GUI explorer, and a command line renderer. 60 | The GUI lets you zoom in and out, and change the color palette to find an image 61 | you like. The command line renderer produces higher-quality images. 62 |

63 | 64 |

Parameter files

65 | 66 |

Aptus stores information about the image to display in a few ways. Small 67 | textual .aptus files store all the parameters needed to recreate an image, 68 | and can be saved from the GUI and fed to the command line renderer.

69 | 70 |

When saving an image as a .PNG file, Aptus also stores all its parameter 71 | information in a text block hidden in the image, so that the .PNG can be used 72 | directly as a parameter file. 73 |

74 | 75 |

Aptus can also read Xaos 76 | .xpf files so that you can use Xaos to explore, and Aptus to render if you like.

77 | 78 |

GUI usage

79 | 80 |

Start Aptus with aptusgui.py, and start poking around. Left click or drag 81 | zooms you in, right click zooms you out. Type 'h' for help on other controls. 82 | Detailed descriptions of GUI behavior are below.

83 | 84 |

aptusgui.py also accepts applicable command-line switches so you can start it from a 85 | parameter file, or specify the size of the window, and so on.

86 | 87 | 88 |

Command line usage

89 | 90 |

The command line renderer is called aptuscmd.py. It will accept a number of 91 | switches or parameter files: 92 |

93 | 94 | 95 | Usage: aptuscmd.py [options] [parameterfile] 96 | 97 | Aptus renders Mandelbrot set images. Two flavors are available: aptusgui.py 98 | for interactive exploration, and aptuscmd.py for high-quality rendering. 99 | 100 | Options: 101 | -h, --help show this help message and exit 102 | -a ANGLE, --angle=ANGLE 103 | set the angle of rotation 104 | --center=RE,IM set the center of the view 105 | -c, --continuous use continuous coloring 106 | --diam=DIAM set the diameter of the view 107 | -i ITER_LIMIT, --iterlimit=ITER_LIMIT 108 | set the limit on the iteration count 109 | -o OUTFILE, --output=OUTFILE 110 | set the output filename (aptuscmd.py only) 111 | --phase=PHASE set the palette phase 112 | --pscale=SCALE set the palette scale 113 | -s WIDxHGT, --size=WIDxHGT 114 | set the pixel size of the image 115 | --super=S set the supersample rate (aptuscmd.py only) 116 | 117 | 118 | 119 |

GUI controls

120 | 121 |

The Aptus GUI is very bare: there's just an image of the Mandelbrot set, with 122 | no visible controls. You use the mouse and keyboard to control Aptus.

123 | 124 |

Moving around

125 | 126 |

Left-clicking zooms into the set, right-clicking zooms out; clicking while 127 | holding the Ctrl (or Cmd) key zooms just a little bit. If you drag out a 128 | rectangle with the left mouse button, you will zoom into that rectangle, so you 129 | have more control over exactly where you end up.

130 | 131 |

If you drag with the middle mouse button, you will drag the image, re-centering 132 | it on a new point of interest.

133 | 134 |

The 'a' key will prompt you for a new angle of rotation for the image.

135 | 136 |

The 'n' key will open a new top-level window to explore elsewhere in the set.

137 | 138 |

Appearance

139 | 140 |

The image of the Mandelbrot set is drawn by calculating a value at each pixel, 141 | then mapping that value to a color through a palette. The values can be discrete 142 | or continuous use the 'c' key to toggle between the two.

143 | 144 |

The accuracy of the black boundary of the set depends on the number of iterations 145 | Aptus is permitted to calculate at each point. The value can be adjusted with 146 | the 'i' key.

147 | 148 |

Aptus has handful of different palettes. Cycle through them with the '<' 149 | (less-than) and '>' (greater-than) keys. A list of all the palettes can be 150 | displayed with 'p'. The color mapped to each value can be shifted one color with 151 | the ',' (comma) and '.' (period) keys. If the pixel values are continuous, then 152 | the palette can also be scaled to change the distance between color bands 153 | use the ';' (semicolon) and "'" (apostrophe) keys, optionally with the 154 | Ctrl key to change just a little.

155 | 156 |

The hue and saturation of the palette can also be shifted. The '[' and ']' 157 | (square bracket) keys change the hue, and '{' and '}' (curly braces) change the 158 | saturation. Both also use the Ctrl key to change just a little.

159 | 160 |

The '0' (zero) key will reset all palette adjustments.

161 | 162 |

Auxiliary windows

163 | 164 |

Aptus has a few tool windows. Each is toggled with a particular key.

165 | 166 |

'p' displays a list of all Aptus' palettes. Clicking one will change 167 | the display to use it.

168 | 169 |

'v' displays a list of statistics about the last fractal calculation.

170 | 171 |

'q' displays information about a point in the display. Hold the shift key 172 | and hover over a point in the image to see iteration counts, coordinates, and so on.

173 | 174 |

'l' (lowercase L) displays the You Are Here panel. It shows a series of images, 175 | zooming in to the currently displayed view of the set. Each image has a rectangle 176 | drawn on it corresponding to the next image in the list, so that you can see 177 | how your close-up view in the main window relates to the larger set. Any rectangle 178 | can be dragged to change the main window's view of the set.

179 | 180 | 181 |

Julia set

182 | 183 |

The Julia set is closely related to the Mandelbrot set. Each point in the 184 | Mandelbrot set corresponds to a Julia set. To display the Julia set, use the 'J' 185 | key (with the shift key). A small tool window appears. It shows the Julia set 186 | for the current shift-hovered point in the main window. Hold the shift key and 187 | move the mouse over the Mandelbrot set. The Julia set will change as the mouse 188 | moves.

189 | 190 |

To further explore a particular Julia set, double-click in the Julia set window. 191 | You'll get a new top-level window displaying the Julia set, and you can use all 192 | the usual Aptus controls to navigate and manipulate the image.

193 | 194 | 195 |

History

196 | 197 |

Version 2.0, October 2008

198 | 199 |
    200 |
  • Multiple top-level exploration windows.
  • 201 | 202 |
  • Tool panels show supplementary information: 203 |
      204 |
    • You Are Here shows your location in the Mandelbrot set.
    • 205 |
    • Palettes panel shows all the palettes, and the one currently in use.
    • 206 |
    • Statistics panel shows statistics about the latest computation.
    • 207 |
    • Point Info panel shows information about the current point, shift-hover to indicate point.
    • 208 |
    • Julia panel shows Julia set for the current point, shift-hover to indicate point. 209 | Double-clicking the Julia panel opens a new exploration window to explore that Julia set.
    • 210 |
  • 211 | 212 |
  • Computation improvements: 213 |
      214 |
    • Faster.
    • 215 |
    • The exploration window updates during computation.
    • 216 |
    • Continuous coloring is more accurate now: banding artifacts are gone.
    • 217 |
    • When dragging the exploration window, pixels still in the window aren't re-calculated.
    • 218 |
  • 219 | 220 |
  • Center and diameter can be specified in the command line arguments.
  • 221 |
222 | 223 |

Version 1.56, April 2008

224 | 225 |

More display improvements and simplifications. Thanks, Paul Ollis.

226 | 227 | 228 |

Version 1.55, April 2008

229 | 230 |

The display is now flicker-free. Thanks, Rob McMullen.

231 | 232 | 233 |

Version 1.51, March 2008

234 | 235 |

Fixed a few bugs, including not drawing at all on Mac or Linux!

236 | 237 | 238 |

Version 1.5, March 2008

239 | 240 |

A number of improvements:

241 | 242 |
    243 |
  • Continuous coloring.
  • 244 | 245 |
  • Arbitrary rotation support.
  • 246 | 247 |
  • Middle mouse button drags the image.
  • 248 | 249 |
  • Palette tweaking: 250 |
      251 |
    • Hue and saturation adjustments.
    • 252 |
    • Scaling the palette to adjust distance between colors.
    • 253 |
  • 254 | 255 |
  • Statistics: 256 |
      257 |
    • More statistics: boundaries traced, boundaries filled, and points computed.
    • 258 |
    • Statistics are written into the final .PNG files.
    • 259 |
  • 260 | 261 |
  • Reads .xet files from servebeer.com.
  • 262 | 263 |
  • Some old .aptus files recorded the y coordinate incorrectly, 264 | and will now render upside-down: negate the y component of the center to fix this.
  • 265 |
266 | 267 | 268 |

Version 1.0, October 2007

269 | 270 |

First version.

271 | 272 | 273 |

More samples

274 | 275 | 276 | 277 | 278 |
279 | 280 | 281 |
282 | 283 |

See also

284 | 285 |
    286 |
  • Xaos, a full-featured 287 | fractal explorer which has many more rendering and fractal options than Aptus.
  • 288 |
  • Gnofract 4D, a Linux-based 289 | fractal exploration program.
  • 290 |
  • My blog, where recreational math and Python 291 | topics intersect from time to time.
  • 292 |
293 | 294 | 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /src/aptus/gui/viewpanel.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | from aptus import settings 4 | from aptus.gui.computepanel import ComputePanel 5 | from aptus.gui.ids import * 6 | from aptus.palettes import all_palettes 7 | from aptus.progress import ConsoleProgressReporter, IntervalProgressReporter 8 | 9 | 10 | # A pre-set list of places to visit, with the j command. 11 | JUMPS = [ 12 | (settings.center(), settings.diam()), 13 | ((-1.8605294939875601,-1.0475516319329809e-005), (2.288818359375e-005,2.288818359375e-005)), 14 | ((-1.8605327731370924,-1.2700557708795141e-005), (1.7881393432617188e-007,1.7881393432617188e-007)), 15 | ((0.45687170535326038,0.34780396997928614), (0.005859375,0.005859375)), 16 | ] 17 | 18 | 19 | class AptusViewPanel(ComputePanel): 20 | """ A panel implementing the primary Aptus view and controller. 21 | """ 22 | def __init__(self, parent): 23 | ComputePanel.__init__(self, parent) 24 | 25 | self.compute.quiet = False 26 | 27 | # Bind input events. 28 | self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) 29 | self.Bind(wx.EVT_MIDDLE_DOWN, self.on_middle_down) 30 | self.Bind(wx.EVT_MOTION, self.on_motion) 31 | self.Bind(wx.EVT_LEFT_UP, self.on_left_up) 32 | self.Bind(wx.EVT_MIDDLE_UP, self.on_middle_up) 33 | self.Bind(wx.EVT_RIGHT_UP, self.on_right_up) 34 | self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave_window) 35 | self.Bind(wx.EVT_KEY_DOWN, self.on_key_down) 36 | self.Bind(wx.EVT_KEY_UP, self.on_key_up) 37 | self.Bind(wx.EVT_KILL_FOCUS, self.on_kill_focus) 38 | self.Bind(wx.EVT_SET_FOCUS, self.on_set_focus) 39 | 40 | self.Bind(wx.EVT_MENU, self.cmd_set_angle, id=id_set_angle) 41 | self.Bind(wx.EVT_MENU, self.cmd_set_iter_limit, id=id_set_iter_limit) 42 | self.Bind(wx.EVT_MENU, self.cmd_toggle_continuous, id=id_toggle_continuous) 43 | self.Bind(wx.EVT_MENU, self.cmd_jump, id=id_jump) 44 | self.Bind(wx.EVT_MENU, self.cmd_redraw, id=id_redraw) 45 | self.Bind(wx.EVT_MENU, self.cmd_change_palette, id=id_change_palette) 46 | self.Bind(wx.EVT_MENU, self.cmd_set_palette, id=id_set_palette) 47 | self.Bind(wx.EVT_MENU, self.cmd_cycle_palette, id=id_cycle_palette) 48 | self.Bind(wx.EVT_MENU, self.cmd_scale_palette, id=id_scale_palette) 49 | self.Bind(wx.EVT_MENU, self.cmd_adjust_palette, id=id_adjust_palette) 50 | self.Bind(wx.EVT_MENU, self.cmd_reset_palette, id=id_reset_palette) 51 | 52 | self.reset_mousing() 53 | 54 | # Gui state values 55 | self.palette_index = 0 # The index of the currently displayed palette 56 | self.jump_index = 0 # The index of the last jumped-to spot. 57 | self.zoom = 2.0 # A constant zoom amt per click. 58 | 59 | # Input methods 60 | 61 | def reset_mousing(self): 62 | """ Set all the mousing variables to turn off rubberbanding and panning. 63 | """ 64 | self.pt_down = None 65 | self.rubberbanding = False 66 | self.rubberrect = None 67 | # Panning information. 68 | self.panning = False 69 | self.pt_pan = None 70 | self.pan_locked = False 71 | 72 | # When shift is down, then we're indicating points. 73 | self.indicating_pt = False 74 | self.indicated_pt = (-1, -1) 75 | 76 | def finish_panning(self, mx, my): 77 | if not self.pt_down: 78 | return 79 | cx, cy = self.compute.size[0]/2.0, self.compute.size[1]/2.0 80 | cx -= mx - self.pt_down[0] 81 | cy -= my - self.pt_down[1] 82 | self.compute.center = self.compute.coords_from_pixel(cx, cy) 83 | self.geometry_changed() 84 | 85 | def xor_rectangle(self, rect): 86 | dc = wx.ClientDC(self) 87 | dc.SetLogicalFunction(wx.XOR) 88 | dc.SetBrush(wx.Brush(wx.WHITE, wx.TRANSPARENT)) 89 | dc.SetPen(wx.Pen(wx.WHITE, 1, wx.SOLID)) 90 | dc.DrawRectangle(*rect) 91 | 92 | def set_cursor(self, event_unused): 93 | # If we aren't taking input, then we shouldn't change the cursor. 94 | if not self.GetTopLevelParent().IsActive(): 95 | return 96 | 97 | # Set the proper cursor: 98 | if self.rubberbanding: 99 | self.SetCursor(wx.Cursor(wx.CURSOR_MAGNIFIER)) 100 | elif self.panning: 101 | self.SetCursor(wx.Cursor(wx.CURSOR_SIZING)) 102 | elif self.indicating_pt: 103 | import aptus.gui.resources 104 | curimg = aptus.gui.resources.getCrosshairImage() 105 | curimg.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 7) 106 | curimg.SetOption(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 7) 107 | cur = wx.Cursor(curimg) 108 | self.SetCursor(cur) 109 | #self.SetCursor(wx.Cursor(wx.CURSOR_CROSS)) 110 | else: 111 | self.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) 112 | 113 | def indicate_point(self, event): 114 | """ Use the given event to indicate a point, maybe. 115 | """ 116 | if hasattr(event, 'ShiftDown'): 117 | self.indicating_pt = event.ShiftDown() 118 | else: 119 | self.indicating_pt = wx.GetMouseState().shiftDown 120 | 121 | if self.indicating_pt: 122 | if hasattr(event, 'GetPosition'): 123 | pt = event.GetPosition() 124 | else: 125 | ms = wx.GetMouseState() 126 | pt = self.ScreenToClient((ms.x, ms.y)) 127 | if self.GetRect().Contains(pt) and pt != self.indicated_pt: 128 | self.indicated_pt = pt 129 | self.fire_event(AptusIndicatePointEvent, point=pt) 130 | 131 | def dilate_view(self, center, scale): 132 | """ Change the view by a certain scale factor, keeping the center in the 133 | same spot. 134 | """ 135 | cx = center[0] + (self.compute.size[0]/2 - center[0]) * scale 136 | cy = center[1] + (self.compute.size[1]/2 - center[1]) * scale 137 | self.compute.center = self.compute.coords_from_pixel(cx, cy) 138 | self.compute.diam = (self.compute.diam[0]*scale, self.compute.diam[1]*scale) 139 | self.geometry_changed() 140 | 141 | def make_progress_reporter(self): 142 | # Construct a progress reporter that suits us. Write to the console, 143 | # but only once a second. 144 | return IntervalProgressReporter(1, ConsoleProgressReporter()) 145 | 146 | # Event handlers 147 | 148 | def on_idle(self, event): 149 | self.indicate_point(event) 150 | self.set_cursor(event) 151 | ComputePanel.on_idle(self, event) 152 | 153 | def on_paint(self, event_unused): 154 | if not self.bitmap: 155 | self.bitmap = self.draw_bitmap() 156 | 157 | dc = wx.AutoBufferedPaintDC(self) 158 | if self.panning: 159 | dc.SetBrush(wx.Brush(wx.Colour(224,224,128), wx.SOLID)) 160 | dc.SetPen(wx.Pen(wx.Colour(224,224,128), 1, wx.SOLID)) 161 | dc.DrawRectangle(0, 0, self.compute.size[0], self.compute.size[1]) 162 | dc.DrawBitmap(self.bitmap, self.pt_pan[0]-self.pt_down[0], self.pt_pan[1]-self.pt_down[1], False) 163 | else: 164 | dc.DrawBitmap(self.bitmap, 0, 0, False) 165 | 166 | def on_left_down(self, event): 167 | #print(wx.Window.FindFocus()) 168 | self.pt_down = event.GetPosition() 169 | self.rubberbanding = False 170 | if self.panning: 171 | self.pt_pan = self.pt_down 172 | self.pan_locked = False 173 | 174 | def on_middle_down(self, event): 175 | self.pt_down = event.GetPosition() 176 | self.rubberbanding = False 177 | self.panning = True 178 | self.pt_pan = self.pt_down 179 | self.pan_locked = False 180 | 181 | def on_motion(self, event): 182 | self.indicate_point(event) 183 | self.set_cursor(event) 184 | 185 | # We do nothing with mouse moves that aren't dragging. 186 | if not self.pt_down: 187 | return 188 | 189 | mx, my = event.GetPosition() 190 | 191 | if self.panning: 192 | if self.pt_pan != (mx, my): 193 | # We've moved the image: redraw it. 194 | self.pt_pan = (mx, my) 195 | self.pan_locked = True 196 | self.Refresh() 197 | else: 198 | if not self.rubberbanding: 199 | # Start rubberbanding when we have a 10-pixel rectangle at least. 200 | if abs(self.pt_down[0] - mx) > 10 or abs(self.pt_down[1] - my) > 10: 201 | self.rubberbanding = True 202 | 203 | if self.rubberbanding: 204 | if self.rubberrect: 205 | # Erase the old rectangle. 206 | self.xor_rectangle(self.rubberrect) 207 | 208 | self.rubberrect = (self.pt_down[0], self.pt_down[1], mx-self.pt_down[0], my-self.pt_down[1]) 209 | self.xor_rectangle(self.rubberrect) 210 | 211 | def on_left_up(self, event): 212 | mx, my = event.GetPosition() 213 | if self.rubberbanding: 214 | # Set a new view that encloses the rectangle. 215 | px, py = self.pt_down 216 | ulr, uli = self.compute.coords_from_pixel(px, py) 217 | lrr, lri = self.compute.coords_from_pixel(mx, my) 218 | self.set_geometry(corners=(ulr, uli, lrr, lri)) 219 | elif self.panning: 220 | self.finish_panning(mx, my) 221 | elif self.pt_down: 222 | # Single-click: zoom in. 223 | scale = self.zoom 224 | if event.CmdDown(): 225 | scale = (scale - 1.0)/10 + 1.0 226 | self.dilate_view((mx, my), 1.0/scale) 227 | 228 | self.reset_mousing() 229 | 230 | def on_middle_up(self, event): 231 | self.finish_panning(*event.GetPosition()) 232 | self.reset_mousing() 233 | 234 | def on_right_up(self, event): 235 | scale = self.zoom 236 | if event.CmdDown(): 237 | scale = (scale - 1.0)/10 + 1.0 238 | self.dilate_view(event.GetPosition(), scale) 239 | self.reset_mousing() 240 | 241 | def on_leave_window(self, event): 242 | if self.rubberrect: 243 | self.xor_rectangle(self.rubberrect) 244 | if self.panning: 245 | self.finish_panning(*event.GetPosition()) 246 | self.reset_mousing() 247 | 248 | def on_key_down(self, event): 249 | # Turn keystrokes into commands. 250 | shift = event.ShiftDown() 251 | cmd = event.CmdDown() 252 | keycode = event.KeyCode 253 | #print("Look:", keycode) 254 | if keycode == ord('A'): 255 | self.fire_command(id_set_angle) 256 | elif keycode == ord('C'): 257 | self.fire_command(id_toggle_continuous) 258 | elif keycode == ord('F'): 259 | self.fire_command(id_fullscreen) 260 | elif keycode == ord('H'): 261 | self.fire_command(id_help) 262 | elif keycode == ord('I'): 263 | self.fire_command(id_set_iter_limit) 264 | elif keycode == ord('J'): 265 | if shift: 266 | self.fire_command(id_show_julia) 267 | else: 268 | self.fire_command(id_jump) 269 | elif keycode == ord('L'): 270 | self.fire_command(id_show_youarehere) 271 | elif keycode == ord('N'): 272 | self.fire_command(id_new) 273 | elif keycode == ord('O'): 274 | self.fire_command(id_open) 275 | elif keycode == ord('P'): 276 | self.fire_command(id_show_palettes) 277 | elif keycode == ord('Q'): 278 | self.fire_command(id_show_pointinfo) 279 | elif keycode == ord('R'): 280 | self.fire_command(id_redraw) 281 | elif keycode == ord('S'): 282 | self.fire_command(id_save) 283 | elif keycode == ord('V'): 284 | self.fire_command(id_show_stats) 285 | elif keycode == ord('W'): 286 | self.fire_command(id_window_size) 287 | 288 | elif keycode == ord('0'): # zero 289 | self.fire_command(id_reset_palette) 290 | 291 | elif keycode in [ord(','), ord('<')]: 292 | if shift: 293 | self.fire_command(id_change_palette, -1) 294 | else: 295 | self.fire_command(id_cycle_palette, -1) 296 | elif keycode in [ord('.'), ord('>')]: 297 | if shift: 298 | self.fire_command(id_change_palette, 1) 299 | else: 300 | self.fire_command(id_cycle_palette, 1) 301 | elif keycode == ord(';'): 302 | self.fire_command(id_scale_palette, 1/(1.01 if cmd else 1.1)) 303 | elif keycode == ord("'"): 304 | self.fire_command(id_scale_palette, 1.01 if cmd else 1.1) 305 | elif keycode in [ord('['), ord(']')]: 306 | kw = 'hue' 307 | delta = 1 if cmd else 10 308 | if keycode == ord('['): 309 | delta = -delta 310 | if shift: 311 | kw = 'saturation' 312 | self.fire_command(id_adjust_palette, {kw:delta}) 313 | elif keycode == ord(' '): 314 | self.panning = True 315 | elif keycode == ord('/') and shift: 316 | self.fire_command(id_help) 317 | elif 0: 318 | # Debugging aid: find the symbol for the key we didn't handle. 319 | revmap = dict([(getattr(wx,n), n) for n in dir(wx) if n.startswith('WXK')]) 320 | sym = revmap.get(keycode, "") 321 | if not sym: 322 | sym = "ord(%r)" % chr(keycode) 323 | #print("Unmapped key: %r, %s, shift=%r, cmd=%r" % (keycode, sym, shift, cmd)) 324 | 325 | def on_key_up(self, event): 326 | keycode = event.KeyCode 327 | if keycode == ord(' '): 328 | if not self.pan_locked: 329 | self.panning = False 330 | 331 | def on_set_focus(self, event): 332 | pass #print("Set focus") 333 | 334 | def on_kill_focus(self, event): 335 | return 336 | import traceback; traceback.print_stack() 337 | print("Kill focus to %r" % event.GetWindow()) 338 | print("Parent: %r" % self.GetParent()) 339 | if self.GetParent(): 340 | print("Isactive: %r" % self.GetParent().IsActive()) 341 | 342 | # Command helpers 343 | 344 | def set_value(self, dtitle, dprompt, attr, caster, when_done): 345 | cur_val = getattr(self.compute, attr) 346 | dlg = wx.TextEntryDialog(self.GetTopLevelParent(), dtitle, dprompt, str(cur_val)) 347 | 348 | if dlg.ShowModal() == wx.ID_OK: 349 | try: 350 | setattr(self.compute, attr, caster(dlg.GetValue())) 351 | when_done() 352 | except ValueError as e: 353 | self.message("Couldn't set %s: %s" % (attr, e)) 354 | 355 | dlg.Destroy() 356 | 357 | def palette_changed(self): 358 | """ Use the self.palette_index to set a new palette. 359 | """ 360 | self.compute.palette = all_palettes[self.palette_index] 361 | self.compute.palette_phase = 0 362 | self.compute.palette_scale = 1.0 363 | self.coloring_changed() 364 | 365 | # Commands 366 | 367 | def cmd_set_angle(self, event_unused): 368 | self.set_value('Angle:', 'Set the angle of rotation', 'angle', float, self.geometry_changed) 369 | 370 | def cmd_set_iter_limit(self, event_unused): 371 | self.set_value('Iteration limit:', 'Set the iteration limit', 'iter_limit', int, self.computation_changed) 372 | 373 | def cmd_toggle_continuous(self, event_unused): 374 | self.compute.continuous = not self.compute.continuous 375 | self.computation_changed() 376 | 377 | def cmd_redraw(self, event_unused): 378 | self.compute.clear_results() 379 | self.set_view() 380 | 381 | def cmd_jump(self, event_unused): 382 | self.jump_index += 1 383 | self.jump_index %= len(JUMPS) 384 | self.compute.center, self.compute.diam = JUMPS[self.jump_index] 385 | self.geometry_changed() 386 | 387 | def cmd_cycle_palette(self, event): 388 | delta = event.GetClientData() 389 | self.compute.palette_phase += delta 390 | self.coloring_changed() 391 | 392 | def cmd_scale_palette(self, event): 393 | factor = event.GetClientData() 394 | if self.compute.continuous: 395 | self.compute.palette_scale *= factor 396 | self.coloring_changed() 397 | 398 | def cmd_change_palette(self, event): 399 | delta = event.GetClientData() 400 | self.palette_index += delta 401 | self.palette_index %= len(all_palettes) 402 | self.palette_changed() 403 | 404 | def cmd_set_palette(self, event): 405 | self.palette_index = event.GetClientData() 406 | self.palette_changed() 407 | 408 | def cmd_adjust_palette(self, event): 409 | self.compute.palette.adjust(**event.GetClientData()) 410 | self.coloring_changed() 411 | 412 | def cmd_reset_palette(self, event_unused): 413 | self.compute.palette_phase = 0 414 | self.compute.palette_scale = 1.0 415 | self.compute.palette.reset() 416 | self.coloring_changed() 417 | --------------------------------------------------------------------------------