[vispy] 01/02: Imported Upstream version 0.4.0

Frédéric-Emmanuel Picca picca at moszumanska.debian.org
Thu Jul 2 04:54:31 UTC 2015


This is an automated email from the git hooks/post-receive script.

picca pushed a commit to branch master
in repository vispy.

commit 972f0c8604497ddb34e83ac77a8fe35d3064bef7
Author: Picca Frédéric-Emmanuel <picca at debian.org>
Date:   Wed Jul 1 21:48:45 2015 +0200

    Imported Upstream version 0.4.0
---
 .coveragerc                                        |   11 +-
 .gitignore                                         |    3 +-
 .travis.yml                                        |   70 +-
 Makefile                                           |   13 +-
 README.rst                                         |   15 +-
 appveyor.yml                                       |   36 +
 codegen/annotations.py                             |  103 +-
 codegen/createglapi.py                             |   60 +-
 codegen/get-deprecated.py                          |   69 +
 codegen/headerparser.py                            |    5 +-
 doc/ext/gloooverviewgenerator.py                   |   20 +-
 doc/gloo.rst                                       |   40 +-
 doc/index.rst                                      |   20 +-
 doc/scene.rst                                      |   24 +-
 doc/user_guide.md                                  |  329 ++++
 examples/basics/gloo/animate_images.py             |   16 +-
 examples/basics/gloo/animate_images_slice.py       |   34 +-
 examples/basics/gloo/animate_shape.py              |   12 +-
 examples/basics/gloo/display_lines.py              |   47 +-
 examples/basics/gloo/display_points.py             |   25 +-
 examples/basics/gloo/display_shape.py              |    6 +-
 examples/basics/gloo/gpuimage.py                   |   11 +-
 examples/basics/gloo/hello_fbo.py                  |   24 +-
 examples/basics/gloo/multi_texture.py              |   88 +
 examples/basics/gloo/post_processing.py            |   46 +-
 examples/basics/gloo/rotate_cube.py                |   37 +-
 examples/basics/gloo/start.py                      |   12 +-
 examples/basics/gloo/start_shaders.py              |   26 +-
 examples/basics/plotting/export.py                 |   31 +
 examples/basics/plotting/mpl_plot.py               |   13 +-
 examples/basics/plotting/scatter_histogram.py      |   25 +
 examples/basics/plotting/spectrogram.py            |   26 +
 examples/basics/plotting/vispy_plot.py             |   18 -
 examples/basics/plotting/volume.py                 |   31 +
 examples/basics/scene/background_borders.py        |   22 +
 examples/basics/scene/colored_line.py              |   49 +
 examples/basics/scene/console.py                   |   47 +
 examples/basics/scene/cube.py                      |   23 +
 examples/basics/scene/flipped_axis.py              |   87 +
 examples/basics/scene/grid.py                      |   21 +-
 examples/basics/scene/grid_large.py                |    9 +-
 examples/basics/scene/image.py                     |    8 +-
 examples/basics/scene/isocurve.py                  |   18 +-
 examples/basics/scene/isocurve_for_trisurface.py   |   46 +
 examples/basics/scene/isosurface.py                |   22 +-
 examples/basics/scene/line.py                      |   10 +-
 examples/basics/scene/line_update.py               |   41 +
 examples/basics/scene/modular_shaders/editor.py    |   11 +-
 examples/basics/scene/modular_shaders/sandbox.py   |   39 +-
 examples/basics/scene/nested_viewbox.py            |   25 +-
 examples/basics/scene/one_cam_two_scenes.py        |   51 +
 examples/basics/scene/one_scene_four_cams.py       |   70 +
 examples/basics/scene/sensitivity.py               |   43 +
 examples/basics/scene/shared_context.py            |   45 +
 examples/basics/scene/stereo.py                    |   57 +
 examples/basics/scene/surface_plot.py              |   15 +-
 examples/basics/scene/text.py                      |   20 +-
 examples/basics/scene/viewbox.py                   |   62 +-
 examples/basics/scene/volume.py                    |  133 ++
 examples/basics/visuals/arcball.py                 |   76 +
 examples/basics/visuals/cube.py                    |   54 +
 examples/basics/visuals/custom_visual.py           |  116 +-
 examples/basics/visuals/dynamic_polygon.py         |  117 ++
 examples/basics/visuals/image_transforms.py        |   31 +-
 examples/basics/visuals/image_visual.py            |   25 +-
 examples/basics/visuals/line.py                    |   79 +-
 examples/basics/visuals/line_plot.py               |   32 +-
 examples/basics/visuals/line_transform.py          |   39 +-
 examples/basics/visuals/line_update.py             |   46 -
 examples/basics/visuals/markers.py                 |   42 +-
 examples/basics/visuals/mesh.py                    |   58 +-
 examples/basics/visuals/modular_components.py      |  179 --
 examples/basics/visuals/modular_line.py            |   44 -
 examples/basics/visuals/modular_mesh.py            |  126 --
 examples/basics/visuals/modular_point.py           |   39 -
 examples/basics/visuals/polygon_visual.py          |   74 +-
 examples/basics/visuals/reactive_ellipse.py        |   47 -
 examples/basics/visuals/reactive_polygon.py        |   58 -
 .../basics/visuals/reactive_regular_polygon.py     |   55 -
 examples/basics/visuals/rescalingmarkers.py        |   62 +
 examples/basics/visuals/text_visual.py             |   15 +-
 examples/basics/visuals/tube.py                    |   70 +
 examples/basics/visuals/visual_filters.py          |  108 ++
 examples/benchmark/scene_test_1.py                 |  355 ++++
 examples/benchmark/scene_test_2.py                 |  193 +++
 examples/benchmark/simple_glut.py                  |    3 +-
 examples/benchmark/simple_vispy.py                 |   13 +-
 examples/collections/choropleth.py                 |   84 +
 examples/collections/path_collection.py            |   49 +
 examples/collections/point_collection.py           |   35 +
 examples/collections/polygon_collection.py         |   54 +
 examples/collections/segment_collection.py         |   40 +
 examples/collections/tiger.py                      |   75 +
 examples/collections/triangle_collection.py        |   69 +
 examples/demo/gloo/atom.py                         |   38 +-
 examples/demo/gloo/boids.py                        |  101 +-
 examples/demo/gloo/brain.py                        |   47 +-
 examples/demo/gloo/camera.py                       |   20 +-
 examples/demo/gloo/cloud.py                        |   67 +-
 examples/demo/gloo/donut.py                        |   78 +-
 examples/demo/gloo/fireworks.py                    |   33 +-
 examples/demo/gloo/galaxy.py                       |   61 +-
 examples/demo/gloo/galaxy/galaxy.py                |  205 +++
 examples/demo/gloo/galaxy/galaxy_simulation.py     |  235 +++
 examples/demo/gloo/galaxy/galaxy_specrend.py       |  422 +++++
 examples/demo/gloo/game_of_life.py                 |   24 +-
 examples/demo/gloo/glsl_sandbox_cube.py            |   59 +-
 examples/demo/gloo/graph.py                        |   50 +-
 examples/demo/gloo/grayscott.py                    |   21 +-
 examples/demo/gloo/high_frequency.py               |  119 ++
 examples/demo/gloo/imshow.py                       |   20 +-
 examples/demo/gloo/imshow_cuts.py                  |   32 +-
 examples/demo/gloo/jfa/jfa_translation.py          |    5 +
 examples/demo/gloo/jfa/jfa_vispy.py                |   70 +-
 examples/demo/gloo/mandelbrot.py                   |   95 +-
 .../gloo/{mandelbrot.py => mandelbrot_double.py}   |  176 +-
 examples/demo/gloo/markers.py                      |  232 ---
 examples/demo/gloo/molecular_viewer.py             |   36 +-
 examples/demo/gloo/ndscatter.py                    |   93 +-
 examples/demo/gloo/offscreen.py                    |   21 +-
 examples/demo/gloo/primitive_mesh_viewer_qt.py     |  378 +++++
 examples/demo/gloo/quiver.py                       |  292 ++++
 examples/demo/gloo/rain.py                         |   19 +-
 examples/demo/gloo/raytracing.py                   |   63 +-
 examples/demo/gloo/realtime_signals.py             |   51 +-
 examples/demo/gloo/shadertoy.py                    |  441 +++++
 examples/demo/gloo/show_markers.py                 |  115 --
 examples/demo/gloo/signals.py                      |   35 +-
 examples/demo/gloo/spacy.py                        |   66 +-
 examples/demo/gloo/terrain.py                      |   38 +-
 examples/demo/gloo/unstructured_2d.py              |   58 +-
 examples/demo/gloo/voronoi.py                      |   35 +-
 examples/demo/scene/isocurve_for_trisurface_qt.py  |  129 ++
 examples/demo/scene/magnify.py                     |   97 ++
 examples/ipynb/colormaps.ipynb                     |  163 ++
 examples/ipynb/display_points.ipynb                |  152 --
 examples/ipynb/display_shape.ipynb                 |  116 --
 examples/ipynb/donut.ipynb                         |  255 ---
 examples/ipynb/fireworks.ipynb                     |  195 ---
 examples/ipynb/galaxy.ipynb                        |  238 ---
 examples/ipynb/mandelbrot.ipynb                    |  251 ---
 examples/ipynb/post_processing.ipynb               |  228 ---
 examples/ipynb/rain.ipynb                          |  202 ---
 examples/ipynb/spacy.ipynb                         |  220 ---
 examples/ipynb/voronoi.ipynb                       |  172 --
 examples/ipynb/webgl_example_1.ipynb               |  211 +++
 examples/ipynb/webgl_example_2.ipynb               |  270 +++
 examples/tutorial/app/app_events.py                |    9 +-
 examples/tutorial/app/fps.py                       |    8 +-
 examples/tutorial/app/interactive.py               |   54 +
 examples/tutorial/app/shared_context.py            |    7 +-
 examples/tutorial/app/simple.py                    |    8 +-
 examples/tutorial/app/simple_wx.py                 |   56 +
 examples/tutorial/gl/cube.py                       |   47 +-
 examples/tutorial/gl/fireworks.py                  |   29 +-
 examples/tutorial/gl/quad.py                       |   16 +-
 examples/tutorial/gloo/colored_cube.py             |   32 +-
 examples/tutorial/gloo/colored_quad.py             |   10 +-
 examples/tutorial/gloo/lighted_cube.py             |   28 +-
 examples/tutorial/gloo/outlined_cube.py            |   31 +-
 examples/tutorial/gloo/rotating_quad.py            |   12 +-
 examples/tutorial/gloo/texture_precision.py        |  143 ++
 examples/tutorial/gloo/textured_cube.py            |   32 +-
 examples/tutorial/gloo/textured_quad.py            |   14 +-
 examples/tutorial/visuals/T01_basic_visual.py      |  167 ++
 examples/tutorial/visuals/T02_measurements.py      |  227 +++
 examples/tutorial/visuals/T03_antialiasing.py      |  216 +++
 examples/tutorial/visuals/T04_fragment_programs.py |   81 +
 examples/tutorial/visuals/T05_viewer_location.py   |   97 ++
 make/install_python.ps1                            |  125 ++
 make/make.py                                       |   23 +-
 setup.py                                           |   28 +-
 vispy/__init__.py                                  |   10 +-
 vispy/app/__init__.py                              |    4 +-
 vispy/app/_config.py                               |   20 -
 vispy/app/_default_app.py                          |   27 +-
 vispy/app/application.py                           |   76 +-
 vispy/app/backends/__init__.py                     |    9 +-
 vispy/app/backends/_egl.py                         |   80 +-
 vispy/app/backends/_glfw.py                        |  150 +-
 vispy/app/backends/_glut.py                        |  502 ------
 vispy/app/backends/_ipynb_static.py                |   33 +-
 vispy/app/backends/_ipynb_util.py                  |  104 ++
 vispy/app/backends/_ipynb_vnc.py                   |   27 +-
 vispy/app/backends/_ipynb_webgl.py                 |  315 ++++
 vispy/app/backends/_pyglet.py                      |   76 +-
 vispy/app/backends/_pyqt4.py                       |   17 +-
 vispy/app/backends/_pyqt5.py                       |   43 +
 vispy/app/backends/_pyside.py                      |   17 +-
 vispy/app/backends/_qt.py                          |  463 +++--
 vispy/app/backends/_sdl2.py                        |   92 +-
 vispy/app/backends/_template.py                    |   34 +-
 vispy/app/backends/_test.py                        |    2 +-
 vispy/app/backends/_wx.py                          |  196 ++-
 .../line => app/backends/ipython}/__init__.py      |    4 +-
 vispy/app/backends/ipython/_widget.py              |   88 +
 .../shaders => app/backends}/tests/__init__.py     |    0
 vispy/app/backends/tests/test_ipynb_util.py        |  112 ++
 vispy/app/base.py                                  |  194 ++-
 vispy/app/canvas.py                                |  384 +++--
 vispy/app/inputhook.py                             |   75 +
 vispy/app/qt.py                                    |   74 +
 vispy/app/tests/qt-designer.ui                     |    6 +-
 vispy/app/tests/test_app.py                        |   85 +-
 vispy/app/tests/test_backends.py                   |   33 +-
 vispy/app/tests/test_context.py                    |   77 +-
 vispy/app/tests/test_interactive.py                |   27 +
 vispy/app/tests/test_qt.py                         |   47 +-
 vispy/app/tests/test_simultaneous.py               |   25 +-
 vispy/app/timer.py                                 |   22 +-
 vispy/color/__init__.py                            |   12 +-
 vispy/color/_color_dict.py                         |   17 +-
 vispy/color/{_color.py => color_array.py}          |  272 +--
 vispy/color/color_space.py                         |  183 ++
 vispy/color/colormap.py                            |  564 +++++++
 vispy/color/tests/test_color.py                    |  185 +-
 vispy/ext/cocoapy.py                               |   21 +-
 vispy/ext/cubehelix.py                             |  138 ++
 vispy/ext/decorator.py                             |  253 +++
 vispy/ext/egl.py                                   |    4 +-
 vispy/ext/fontconfig.py                            |    3 +-
 vispy/ext/freetype.py                              |   30 +-
 vispy/ext/gdi32plus.py                             |   19 +-
 vispy/ext/glfw.py                                  |   34 +-
 vispy/ext/gzip_open.py                             |    2 +-
 vispy/ext/ipy_inputhook.py                         |  301 ++++
 vispy/ext/mplutils.py                              |    4 +-
 vispy/ext/ordereddict.py                           |    2 +-
 vispy/ext/py24_ordereddict.py                      |    8 +-
 vispy/ext/six.py                                   |    3 +
 vispy/geometry/__init__.py                         |   16 +-
 vispy/geometry/_triangulation_debugger.py          |   14 +-
 vispy/geometry/calculations.py                     |   58 +-
 vispy/geometry/generation.py                       |  134 +-
 vispy/geometry/isocurve.py                         |    4 +-
 vispy/geometry/isosurface.py                       |    7 +-
 vispy/geometry/meshdata.py                         |  421 +++--
 vispy/geometry/normals.py                          |   82 +
 vispy/geometry/parametric.py                       |   57 +
 vispy/geometry/polygon.py                          |   17 +-
 vispy/geometry/rect.py                             |   56 +-
 vispy/geometry/tests/test_calculations.py          |   24 +
 vispy/geometry/tests/test_generation.py            |   10 +-
 vispy/geometry/tests/test_meshdata.py              |   34 +
 vispy/geometry/tests/test_triangulation.py         |   79 +-
 vispy/geometry/torusknot.py                        |  142 ++
 vispy/geometry/triangulation.py                    |  335 ++--
 vispy/gloo/__init__.py                             |   20 +-
 vispy/gloo/buffer.py                               |  616 ++-----
 vispy/gloo/context.py                              |  258 +++
 vispy/gloo/framebuffer.py                          |  451 ++---
 vispy/gloo/gl/__init__.py                          |   58 +-
 vispy/gloo/gl/{_angle.py => _es2.py}               |   11 +-
 vispy/gloo/gl/{_desktop.py => _gl2.py}             |    9 +-
 vispy/gloo/gl/_proxy.py                            |    4 -
 vispy/gloo/gl/{_pyopengl.py => _pyopengl2.py}      |   39 +-
 vispy/gloo/gl/angle.py                             |   42 -
 vispy/gloo/gl/dummy.py                             |   25 +
 vispy/gloo/gl/es2.py                               |   62 +
 vispy/gloo/gl/{desktop.py => gl2.py}               |   68 +-
 vispy/gloo/gl/glplus.py                            |  169 ++
 vispy/gloo/gl/{pyopengl.py => pyopengl2.py}        |   60 +-
 vispy/gloo/gl/tests/test_basics.py                 |   35 +-
 vispy/gloo/gl/tests/test_functionality.py          |   39 +-
 vispy/gloo/gl/tests/test_names.py                  |   82 +-
 vispy/gloo/gl/tests/test_use.py                    |   47 +-
 vispy/gloo/gl/webgl.py                             |   29 -
 vispy/gloo/glir.py                                 | 1266 ++++++++++++++
 vispy/gloo/globject.py                             |  153 +-
 vispy/gloo/initialize.py                           |   18 -
 vispy/gloo/preprocessor.py                         |   70 +
 vispy/gloo/program.py                              |  815 +++++----
 vispy/gloo/shader.py                               |  284 ----
 vispy/gloo/tests/test_buffer.py                    |  389 +++--
 vispy/gloo/tests/test_context.py                   |  119 ++
 vispy/gloo/tests/test_framebuffer.py               |  177 ++
 vispy/gloo/tests/test_glir.py                      |   89 +
 vispy/gloo/tests/test_globject.py                  |   41 +-
 vispy/gloo/tests/test_program.py                   |  338 +++-
 vispy/gloo/tests/test_shader.py                    |   90 -
 vispy/gloo/tests/test_texture.py                   |  850 ++++++----
 vispy/gloo/tests/test_use_gloo.py                  |  107 +-
 vispy/gloo/tests/test_util.py                      |   60 +
 vispy/gloo/tests/test_variable.py                  |  139 --
 vispy/gloo/tests/test_wrappers.py                  |  174 +-
 vispy/gloo/texture.py                              | 1146 ++++++-------
 vispy/gloo/util.py                                 |   26 +-
 vispy/gloo/variable.py                             |  386 -----
 vispy/gloo/wrappers.py                             | 1154 ++++++-------
 vispy/glsl/__init__.py                             |   44 +
 .../shaders/tests => glsl/antialias}/__init__.py   |    0
 vispy/glsl/antialias/antialias.glsl                |    7 +
 vispy/glsl/antialias/cap-butt.glsl                 |   31 +
 vispy/glsl/antialias/cap-round.glsl                |   29 +
 vispy/glsl/antialias/cap-square.glsl               |   30 +
 vispy/glsl/antialias/cap-triangle-in.glsl          |   30 +
 vispy/glsl/antialias/cap-triangle-out.glsl         |   30 +
 vispy/glsl/antialias/cap.glsl                      |   67 +
 vispy/glsl/antialias/caps.glsl                     |   67 +
 vispy/glsl/antialias/filled.glsl                   |   45 +
 vispy/glsl/antialias/outline.glsl                  |   40 +
 vispy/glsl/antialias/stroke.glsl                   |   43 +
 .../shaders/tests => glsl/arrows}/__init__.py      |    0
 vispy/glsl/arrows/angle-30.glsl                    |   12 +
 vispy/glsl/arrows/angle-60.glsl                    |   12 +
 vispy/glsl/arrows/angle-90.glsl                    |   12 +
 vispy/glsl/arrows/arrow.frag                       |   38 +
 vispy/glsl/arrows/arrow.vert                       |   49 +
 vispy/glsl/arrows/arrows.glsl                      |   17 +
 vispy/glsl/arrows/common.glsl                      |  187 +++
 vispy/glsl/arrows/curved.glsl                      |   63 +
 vispy/glsl/arrows/stealth.glsl                     |   50 +
 vispy/glsl/arrows/triangle-30.glsl                 |   12 +
 vispy/glsl/arrows/triangle-60.glsl                 |   12 +
 vispy/glsl/arrows/triangle-90.glsl                 |   12 +
 vispy/glsl/arrows/util.glsl                        |   98 ++
 vispy/glsl/build-spatial-filters.py                |  675 ++++++++
 .../shaders/tests => glsl/collections}/__init__.py |    0
 vispy/glsl/collections/agg-fast-path.frag          |   20 +
 vispy/glsl/collections/agg-fast-path.vert          |   78 +
 vispy/glsl/collections/agg-glyph.frag              |   60 +
 vispy/glsl/collections/agg-glyph.vert              |   33 +
 vispy/glsl/collections/agg-marker.frag             |   35 +
 vispy/glsl/collections/agg-marker.vert             |   48 +
 vispy/glsl/collections/agg-path.frag               |   55 +
 vispy/glsl/collections/agg-path.vert               |  166 ++
 vispy/glsl/collections/agg-point.frag              |   21 +
 vispy/glsl/collections/agg-point.vert              |   35 +
 vispy/glsl/collections/agg-segment.frag            |   32 +
 vispy/glsl/collections/agg-segment.vert            |   75 +
 vispy/glsl/collections/marker.frag                 |   38 +
 vispy/glsl/collections/marker.vert                 |   48 +
 vispy/glsl/collections/raw-path.frag               |   15 +
 vispy/glsl/collections/raw-path.vert               |   24 +
 vispy/glsl/collections/raw-point.frag              |   14 +
 vispy/glsl/collections/raw-point.vert              |   31 +
 vispy/glsl/collections/raw-segment.frag            |   18 +
 vispy/glsl/collections/raw-segment.vert            |   26 +
 vispy/glsl/collections/raw-triangle.frag           |   13 +
 vispy/glsl/collections/raw-triangle.vert           |   26 +
 vispy/glsl/collections/sdf-glyph-ticks.vert        |   69 +
 vispy/glsl/collections/sdf-glyph.frag              |   80 +
 vispy/glsl/collections/sdf-glyph.vert              |   59 +
 vispy/glsl/collections/tick-labels.vert            |   71 +
 .../shaders/tests => glsl/colormaps}/__init__.py   |    0
 vispy/glsl/colormaps/autumn.glsl                   |   20 +
 vispy/glsl/colormaps/blues.glsl                    |   20 +
 vispy/glsl/colormaps/color-space.glsl              |   17 +
 vispy/glsl/colormaps/colormaps.glsl                |   24 +
 vispy/glsl/colormaps/cool.glsl                     |   20 +
 vispy/glsl/colormaps/fire.glsl                     |   21 +
 vispy/glsl/colormaps/gray.glsl                     |   20 +
 vispy/glsl/colormaps/greens.glsl                   |   20 +
 vispy/glsl/colormaps/hot.glsl                      |   22 +
 vispy/glsl/colormaps/ice.glsl                      |   20 +
 vispy/glsl/colormaps/icefire.glsl                  |   23 +
 vispy/glsl/colormaps/parse.py                      |   38 +
 vispy/glsl/colormaps/reds.glsl                     |   20 +
 vispy/glsl/colormaps/spring.glsl                   |   20 +
 vispy/glsl/colormaps/summer.glsl                   |   20 +
 vispy/glsl/colormaps/user.glsl                     |   22 +
 vispy/glsl/colormaps/util.glsl                     |   41 +
 vispy/glsl/colormaps/wheel.glsl                    |   21 +
 vispy/glsl/colormaps/winter.glsl                   |   20 +
 .../shaders/tests => glsl/markers}/__init__.py     |    0
 vispy/glsl/markers/arrow.glsl                      |   12 +
 vispy/glsl/markers/asterisk.glsl                   |   16 +
 vispy/glsl/markers/chevron.glsl                    |   14 +
 vispy/glsl/markers/clover.glsl                     |   20 +
 vispy/glsl/markers/club.glsl                       |   31 +
 vispy/glsl/markers/cross.glsl                      |   17 +
 vispy/glsl/markers/diamond.glsl                    |   12 +
 vispy/glsl/markers/disc.glsl                       |    9 +
 vispy/glsl/markers/ellipse.glsl                    |   67 +
 vispy/glsl/markers/hbar.glsl                       |    9 +
 vispy/glsl/markers/heart.glsl                      |   15 +
 vispy/glsl/markers/infinity.glsl                   |   15 +
 vispy/glsl/markers/marker-sdf.frag                 |   74 +
 vispy/glsl/markers/marker-sdf.vert                 |   41 +
 vispy/glsl/markers/marker.frag                     |   36 +
 vispy/glsl/markers/marker.vert                     |   46 +
 vispy/glsl/markers/markers.glsl                    |   24 +
 vispy/glsl/markers/pin.glsl                        |   18 +
 vispy/glsl/markers/ring.glsl                       |   11 +
 vispy/glsl/markers/spade.glsl                      |   28 +
 vispy/glsl/markers/square.glsl                     |   10 +
 vispy/glsl/markers/tag.glsl                        |   11 +
 vispy/glsl/markers/triangle.glsl                   |   14 +
 vispy/glsl/markers/vbar.glsl                       |    9 +
 .../{scene/shaders/tests => glsl/math}/__init__.py |    0
 vispy/glsl/math/circle-through-2-points.glsl       |   30 +
 vispy/glsl/math/constants.glsl                     |   48 +
 vispy/glsl/math/double.glsl                        |  114 ++
 vispy/glsl/math/functions.glsl                     |   20 +
 vispy/glsl/math/point-to-line-distance.glsl        |   31 +
 vispy/glsl/math/point-to-line-projection.glsl      |   29 +
 vispy/glsl/math/signed-line-distance.glsl          |   27 +
 vispy/glsl/math/signed-segment-distance.glsl       |   30 +
 .../{scene/shaders/tests => glsl/misc}/__init__.py |    0
 vispy/glsl/misc/regular-grid.frag                  |  244 +++
 vispy/glsl/misc/spatial-filters.frag               |  322 ++++
 vispy/glsl/misc/viewport-NDC.glsl                  |   20 +
 .../shaders/tests => glsl/transforms}/__init__.py  |    0
 vispy/glsl/transforms/azimuthal-equal-area.glsl    |   32 +
 vispy/glsl/transforms/azimuthal-equidistant.glsl   |   38 +
 vispy/glsl/transforms/hammer.glsl                  |   44 +
 vispy/glsl/transforms/identity.glsl                |    6 +
 vispy/glsl/transforms/identity_forward.glsl        |   23 +
 vispy/glsl/transforms/identity_inverse.glsl        |   23 +
 vispy/glsl/transforms/linear-scale.glsl            |  127 ++
 vispy/glsl/transforms/log-scale.glsl               |  126 ++
 .../transforms/mercator-transverse-forward.glsl    |   40 +
 .../transforms/mercator-transverse-inverse.glsl    |   40 +
 vispy/glsl/transforms/panzoom.glsl                 |   10 +
 vispy/glsl/transforms/polar.glsl                   |   41 +
 vispy/glsl/transforms/position.glsl                |   44 +
 vispy/glsl/transforms/power-scale.glsl             |  139 ++
 vispy/glsl/transforms/projection.glsl              |    7 +
 vispy/glsl/transforms/pvm.glsl                     |   13 +
 vispy/glsl/transforms/rotate.glsl                  |   45 +
 vispy/glsl/transforms/trackball.glsl               |   15 +
 vispy/glsl/transforms/translate.glsl               |   35 +
 vispy/glsl/transforms/transverse_mercator.glsl     |   38 +
 vispy/glsl/transforms/viewport-clipping.glsl       |   14 +
 vispy/glsl/transforms/viewport-transform.glsl      |   16 +
 vispy/glsl/transforms/viewport.glsl                |   50 +
 vispy/glsl/transforms/x.glsl                       |   24 +
 vispy/glsl/transforms/y.glsl                       |   19 +
 vispy/glsl/transforms/z.glsl                       |   14 +
 vispy/html/static/js/jquery.mousewheel.min.js      |    8 +
 vispy/html/static/js/vispy.js                      |  190 ---
 vispy/html/static/js/vispy.min.js                  |    2 +
 vispy/html/static/js/webgl-backend.js              |  140 ++
 vispy/io/__init__.py                               |    9 +-
 vispy/io/datasets.py                               |    7 +-
 vispy/io/image.py                                  |    2 +-
 vispy/io/mesh.py                                   |    2 +-
 vispy/io/tests/test_image.py                       |    7 +-
 vispy/io/tests/test_io.py                          |    7 +-
 vispy/io/wavefront.py                              |    2 +-
 vispy/mpl_plot/__init__.py                         |    2 +-
 vispy/mpl_plot/_mpl_to_vispy.py                    |   14 +-
 vispy/mpl_plot/tests/test_show_vispy.py            |    9 +-
 vispy/plot/__init__.py                             |   36 +-
 vispy/plot/fig.py                                  |   53 +
 vispy/plot/plot.py                                 |   37 -
 vispy/plot/plotwidget.py                           |  259 +++
 vispy/{scene/shaders => plot}/tests/__init__.py    |    0
 vispy/plot/tests/test_plot.py                      |   21 +
 vispy/scene/__init__.py                            |   65 +-
 vispy/scene/cameras.py                             |  595 -------
 vispy/scene/cameras/__init__.py                    |   20 +
 vispy/scene/cameras/cameras.py                     | 1762 ++++++++++++++++++++
 vispy/scene/cameras/magnify.py                     |  163 ++
 vispy/scene/cameras/tests/test_perspective.py      |   57 +
 vispy/scene/canvas.py                              |  255 ++-
 vispy/scene/entity.py                              |  331 ----
 vispy/scene/events.py                              |  283 ++--
 vispy/scene/node.py                                |  466 ++++++
 vispy/scene/shaders/program.py                     |   83 -
 vispy/scene/subscene.py                            |   21 +-
 vispy/scene/systems.py                             |   87 +-
 vispy/scene/tests/test_node.py                     |   35 +
 vispy/scene/tests/test_visuals.py                  |   27 +
 vispy/scene/transforms/linear.py                   |  401 -----
 vispy/scene/transforms/nonlinear.py                |  162 --
 vispy/scene/visuals.py                             |  142 ++
 vispy/scene/visuals/__init__.py                    |   35 -
 vispy/scene/visuals/image.py                       |  158 --
 vispy/scene/visuals/line/line.py                   |  424 -----
 vispy/scene/visuals/line_plot.py                   |   96 --
 vispy/scene/visuals/markers.py                     |  334 ----
 vispy/scene/visuals/mesh.py                        |  224 ---
 vispy/scene/visuals/modular_line.py                |   28 -
 vispy/scene/visuals/modular_mesh.py                |   21 -
 vispy/scene/visuals/modular_point.py               |   46 -
 vispy/scene/visuals/modular_visual.py              |  353 ----
 vispy/scene/visuals/polygon.py                     |  126 --
 vispy/scene/visuals/visual.py                      |   81 -
 vispy/scene/widgets/__init__.py                    |    5 +-
 vispy/scene/widgets/anchor.py                      |   10 +-
 vispy/scene/widgets/console.py                     |  299 ++++
 vispy/scene/widgets/grid.py                        |  176 +-
 vispy/scene/widgets/viewbox.py                     |  301 +++-
 vispy/scene/widgets/widget.py                      |  182 +-
 vispy/testing/__init__.py                          |   52 +-
 vispy/testing/_coverage.py                         |   36 -
 vispy/testing/_runners.py                          |  275 ++-
 vispy/testing/_testing.py                          |  362 ++--
 vispy/testing/image_tester.py                      |  454 +++++
 vispy/testing/tests/test_testing.py                |   12 +-
 vispy/util/__init__.py                             |   12 +-
 vispy/util/bunch.py                                |   15 +
 vispy/util/config.py                               |  324 ++--
 vispy/util/dpi/__init__.py                         |   21 +
 vispy/util/dpi/_linux.py                           |   58 +
 vispy/util/dpi/_quartz.py                          |   26 +
 vispy/util/dpi/_win32.py                           |   34 +
 .../{scene/shaders => util/dpi}/tests/__init__.py  |    0
 vispy/util/dpi/tests/test_dpi.py                   |   16 +
 vispy/util/event.py                                |   83 +-
 vispy/util/fetching.py                             |   77 +-
 vispy/util/filter.py                               |    2 +-
 vispy/util/fonts/__init__.py                       |    2 +-
 vispy/util/fonts/_freetype.py                      |    2 +-
 vispy/util/fonts/_quartz.py                        |    2 +-
 vispy/util/fonts/_triage.py                        |    2 +-
 vispy/util/fonts/_vispy_fonts.py                   |    2 +-
 vispy/util/fonts/_win32.py                         |    4 +-
 vispy/util/fonts/tests/test_font.py                |   13 +-
 vispy/util/fourier.py                              |   69 +
 vispy/util/keys.py                                 |    2 +-
 vispy/util/logs.py                                 |   61 +-
 vispy/util/profiler.py                             |  138 ++
 vispy/util/ptime.py                                |    2 +-
 vispy/util/quaternion.py                           |  236 +++
 vispy/util/svg/__init__.py                         |   18 +
 vispy/util/svg/base.py                             |   20 +
 vispy/util/svg/color.py                            |  216 +++
 vispy/util/svg/element.py                          |   52 +
 vispy/util/svg/geometry.py                         |  470 ++++++
 vispy/util/svg/group.py                            |   66 +
 vispy/util/svg/length.py                           |   81 +
 vispy/util/svg/number.py                           |   25 +
 vispy/util/svg/path.py                             |  331 ++++
 vispy/util/svg/shapes.py                           |   57 +
 vispy/util/svg/style.py                            |   59 +
 vispy/util/svg/svg.py                              |   40 +
 vispy/util/svg/transform.py                        |  229 +++
 vispy/util/svg/transformable.py                    |   29 +
 vispy/util/svg/viewport.py                         |   73 +
 vispy/util/tests/test_config.py                    |   17 +-
 vispy/util/tests/test_docstring_parameters.py      |  123 ++
 vispy/util/tests/test_emitter_group.py             |   39 +-
 vispy/util/tests/test_event_emitter.py             |   28 +-
 vispy/util/tests/test_fourier.py                   |   35 +
 vispy/util/tests/test_import.py                    |   63 +-
 vispy/util/tests/test_key.py                       |    9 +-
 vispy/util/tests/test_logging.py                   |   10 +-
 vispy/util/tests/test_run.py                       |    9 +-
 vispy/util/tests/test_transforms.py                |   27 +-
 vispy/util/tests/test_vispy.py                     |   19 +-
 vispy/util/transforms.py                           |  214 +--
 vispy/util/wrappers.py                             |   82 +-
 vispy/visuals/__init__.py                          |   35 +
 vispy/visuals/collections/__init__.py              |   30 +
 .../collections/agg_fast_path_collection.py        |  226 +++
 vispy/visuals/collections/agg_path_collection.py   |  203 +++
 vispy/visuals/collections/agg_point_collection.py  |   54 +
 .../visuals/collections/agg_segment_collection.py  |  147 ++
 vispy/visuals/collections/array_list.py            |  415 +++++
 vispy/visuals/collections/base_collection.py       |  495 ++++++
 vispy/visuals/collections/collection.py            |  250 +++
 vispy/visuals/collections/path_collection.py       |   24 +
 vispy/visuals/collections/point_collection.py      |   20 +
 vispy/visuals/collections/polygon_collection.py    |   26 +
 vispy/visuals/collections/raw_path_collection.py   |  123 ++
 vispy/visuals/collections/raw_point_collection.py  |  115 ++
 .../visuals/collections/raw_polygon_collection.py  |   79 +
 .../visuals/collections/raw_segment_collection.py  |  117 ++
 .../visuals/collections/raw_triangle_collection.py |   81 +
 vispy/visuals/collections/segment_collection.py    |   20 +
 vispy/visuals/collections/triangle_collection.py   |   17 +
 vispy/visuals/collections/util.py                  |  163 ++
 vispy/{scene => visuals}/components/__init__.py    |    6 +-
 vispy/visuals/components/clipper.py                |   53 +
 vispy/{scene => visuals}/components/color.py       |    2 +-
 vispy/visuals/components/color2.py                 |   57 +
 vispy/{scene => visuals}/components/component.py   |    0
 vispy/{scene => visuals}/components/material.py    |    2 +-
 vispy/{scene => visuals}/components/normal.py      |    6 +-
 vispy/{scene => visuals}/components/texture.py     |    4 +-
 vispy/{scene => visuals}/components/vertex.py      |    2 +-
 vispy/visuals/cube.py                              |   54 +
 vispy/{scene => }/visuals/ellipse.py               |   46 +-
 vispy/visuals/glsl/__init__.py                     |    1 +
 vispy/visuals/glsl/antialiasing.py                 |  153 ++
 vispy/visuals/glsl/color.py                        |   70 +
 vispy/{scene => }/visuals/gridlines.py             |   53 +-
 vispy/visuals/histogram.py                         |   58 +
 vispy/visuals/image.py                             |  306 ++++
 vispy/{scene => }/visuals/isocurve.py              |   47 +-
 vispy/visuals/isoline.py                           |  218 +++
 vispy/{scene => }/visuals/isosurface.py            |   37 +-
 vispy/{scene => }/visuals/line/__init__.py         |    4 +-
 vispy/{scene => }/visuals/line/dash_atlas.py       |    2 +-
 vispy/{scene => }/visuals/line/fragment.py         |    0
 vispy/visuals/line/line.py                         |  555 ++++++
 vispy/{scene => }/visuals/line/vertex.py           |    0
 vispy/visuals/line_plot.py                         |  143 ++
 vispy/visuals/markers.py                           |  648 +++++++
 vispy/visuals/mesh.py                              |  348 ++++
 vispy/visuals/polygon.py                           |  128 ++
 vispy/{scene => }/visuals/rectangle.py             |   39 +-
 vispy/{scene => }/visuals/regular_polygon.py       |   50 +-
 vispy/{scene => visuals}/shaders/__init__.py       |    7 +-
 vispy/{scene => visuals}/shaders/compiler.py       |   77 +-
 vispy/visuals/shaders/expression.py                |  100 ++
 vispy/{scene => visuals}/shaders/function.py       |  567 ++-----
 vispy/{scene => visuals}/shaders/parsing.py        |    9 +-
 vispy/visuals/shaders/program.py                   |  125 ++
 vispy/visuals/shaders/shader_object.py             |  164 ++
 vispy/{scene => visuals}/shaders/tests/__init__.py |    0
 .../shaders/tests/test_function.py                 |   59 +-
 .../shaders/tests/test_parsing.py                  |   18 +-
 vispy/visuals/shaders/variable.py                  |  214 +++
 vispy/visuals/spectrogram.py                       |   56 +
 vispy/{scene => }/visuals/surface_plot.py          |   20 +-
 vispy/visuals/tests/test_collections.py            |   16 +
 vispy/{scene => }/visuals/tests/test_ellipse.py    |   60 +-
 vispy/visuals/tests/test_histogram.py              |   25 +
 vispy/visuals/tests/test_image.py                  |   25 +
 vispy/visuals/tests/test_markers.py                |   31 +
 vispy/{scene => }/visuals/tests/test_polygon.py    |   42 +-
 vispy/visuals/tests/test_rectangle.py              |  138 ++
 .../visuals/tests/test_regular_polygon.py          |   54 +-
 vispy/{scene => }/visuals/tests/test_sdf.py        |   16 +-
 vispy/visuals/tests/test_spectrogram.py            |   30 +
 vispy/{scene => }/visuals/tests/test_text.py       |   14 +-
 vispy/visuals/tests/test_volume.py                 |   61 +
 vispy/{scene => }/visuals/text/__init__.py         |    4 +-
 vispy/{scene => }/visuals/text/_sdf.py             |   37 +-
 vispy/{scene => }/visuals/text/text.py             |  102 +-
 vispy/{scene => visuals}/transforms/__init__.py    |   16 +-
 vispy/{scene => visuals}/transforms/_util.py       |  109 +-
 .../transforms/base_transform.py                   |   49 +-
 vispy/{scene => visuals}/transforms/chain.py       |   67 +-
 vispy/visuals/transforms/interactive.py            |   97 ++
 vispy/visuals/transforms/linear.py                 |  575 +++++++
 vispy/visuals/transforms/nonlinear.py              |  402 +++++
 .../transforms/tests/test_transforms.py            |   29 +-
 vispy/visuals/transforms/transform_system.py       |  237 +++
 vispy/visuals/tube.py                              |  170 ++
 vispy/visuals/visual.py                            |  194 +++
 vispy/visuals/volume.py                            |  676 ++++++++
 vispy/{scene => }/visuals/xyz_axis.py              |   10 +-
 636 files changed, 43410 insertions(+), 16690 deletions(-)

diff --git a/.coveragerc b/.coveragerc
index 9391327..30dbd50 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -4,14 +4,13 @@ source = vispy
 #include = */vispy/*
 # Omit fonts/_quartz.py and _win32 for platform specificity
 omit =
-    */setup.py
-    */vispy/datasets/*
     */vispy/app/backends/_egl.py
-    */vispy/gloo/gl/angle.py
-    */vispy/gloo/gl/_angle.py
+    */vispy/gloo/gl/es2.py
+    */vispy/gloo/gl/_es2.py
     */vispy/testing/*
     */vispy/ext/*
-    */vispy/util/geometry/_triangulation_debugger.py
-    */examples/*
+    */vispy/geometry/_triangulation_debugger.py
     */vispy/util/fonts/_quartz.py
     */vispy/util/fonts/_win32.py
+    */vispy/util/dpi/_quartz.py
+    */vispy/util/dpi/_win32.py
diff --git a/.gitignore b/.gitignore
index f8b69c9..8210326 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,9 +7,10 @@ doc/_build
 /_website
 /_gh-pages
 /_images
+/_demo-data
 build
 dist
 MANIFEST
-.coverage
+.coverage*
 htmlcov
 vispy.egg-info
diff --git a/.travis.yml b/.travis.yml
index bf2beb3..55e91f4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,7 @@ language: python
 # it for 2.7, but the Ubuntu system has installable 2.7 Qt4-GL, which
 # allows for more complete testing.
 
+# Size testing can be skipped by adding "[size skip]" within a commit message.
 
 virtualenv:
     system_site_packages: true
@@ -18,17 +19,26 @@ env:
     - PYTHON=2.7 DEPS=full TEST=standard
     - PYTHON=2.7 DEPS=minimal TEST=standard
     - PYTHON=3.4 DEPS=full TEST=standard
+
+    - PYTHON=2.7 DEPS=full TEST=examples  # test examples
+    - PYTHON=3.4 DEPS=full TEST=examples
+
     - PYTHON=3.4 DEPS=minimal TEST=extra  # test file sizes, style, line endings
 
 
 before_install:
     - REDIRECT_TO=/dev/stdout  # change to /dev/null to silence Travis
-    - travis_retry sudo apt-get -qq update;
+    # install a newer cmake since at this time Travis only has version 2.8.7
+    - if [ "${PYTHON}" != "2.6" ] && [ "${DEPS}" == "full" ]; then
+        travis_retry sudo add-apt-repository -y ppa:kalakris/cmake;
+        travis_retry sudo apt-get -y update -qq;
+        travis_retry sudo apt-get -qq -y install cmake;
+      fi;
     - if [ "${PYTHON}" != "2.7" ]; then
-        wget -q http://repo.continuum.io/miniconda/Miniconda-2.2.2-Linux-x86_64.sh -O miniconda.sh;
+        wget -q http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh;
         chmod +x miniconda.sh;
-        ./miniconda.sh -b &> ${REDIRECT_TO};
-        export PATH=/home/$USER/anaconda/bin:$PATH;
+        ./miniconda.sh -b -p ~/anaconda &> ${REDIRECT_TO};
+        export PATH=~/anaconda/bin:$PATH;
         conda update --yes --quiet conda &> ${REDIRECT_TO};
         travis_retry sudo apt-get -qq -y install libgl1-mesa-dri;
       fi;
@@ -46,14 +56,15 @@ before_install:
         cd ~;
         mkdir target-size-clone && cd target-size-clone;
         git init &> ${REDIRECT_TO} && git remote add -t ${TRAVIS_BRANCH} origin git://github.com/${TRAVIS_REPO_SLUG}.git &>${REDIRECT_TO};
-        git fetch origin ${GIT_TARGET_EXTRA} &> ${REDIRECT_TO} && git checkout -qf FETCH_HEAD &> ${REDIRECT_TO} && cd ..;
-        TARGET_SIZE=`du -s target-size-clone | sed -e "s/\t.*//"`;
-        mkdir source-size-clone && cd source-size-clone;
-        git init &> ${REDIRECT_TO} && git remote add -t ${TRAVIS_BRANCH} origin git://github.com/${TRAVIS_REPO_SLUG}.git &> ${REDIRECT_TO};
-        git fetch origin ${GIT_SOURCE_EXTRA} &> ${REDIRECT_TO} && git checkout -qf FETCH_HEAD &> ${REDIRECT_TO} && cd ..;
-        SOURCE_SIZE=`du -s source-size-clone | sed -e "s/\t.*//"`;
-        if [ "${SOURCE_SIZE}" != "${TARGET_SIZE}" ]; then
-          SIZE_DIFF=`expr ${SOURCE_SIZE} - ${TARGET_SIZE}`;
+        git fetch origin ${GIT_TARGET_EXTRA} &> ${REDIRECT_TO} && git checkout -qf FETCH_HEAD &> ${REDIRECT_TO};
+        git tag travis-merge-target &> ${REDIRECT_TO};
+        git gc --aggressive &> ${REDIRECT_TO};
+        TARGET_SIZE=`du -s . | sed -e "s/\t.*//"`;
+        git pull origin ${GIT_SOURCE_EXTRA} &> ${REDIRECT_TO};
+        git gc --aggressive &> ${REDIRECT_TO};
+        MERGE_SIZE=`du -s . | sed -e "s/\t.*//"`;
+        if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then
+          SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`;
         else
           SIZE_DIFF=0;
         fi;
@@ -61,25 +72,22 @@ before_install:
 
 
 install:
-    # Install numpy, nose, flake
+    # Install numpy, flake
     - if [ "${PYTHON}" != "2.7" ]; then
         conda create -n testenv --yes --quiet pip python=$PYTHON > ${REDIRECT_TO};
         source activate testenv > ${REDIRECT_TO};
-        conda install --yes --quiet numpy nose > ${REDIRECT_TO};
+        conda install --yes --quiet numpy nose pytest > ${REDIRECT_TO};
       else
-        travis_retry sudo apt-get -qq -y install python-numpy python-nose python-setuptools > ${REDIRECT_TO};
+        travis_retry sudo apt-get -qq -y install python-numpy python-setuptools > ${REDIRECT_TO};
+        pip install -q pytest > ${REDIRECT_TO};
       fi;
-    - pip install -q coveralls nose-timer
+    - pip install -q coveralls pytest-cov
     # Dont install flake8 on 2.6 to make sure tests still run without it
     - if [ "${PYTHON}" != "2.6" ]; then
         pip install -q flake8;
       else
         pip install -q unittest2;
       fi
-    # helpful for debugging faults
-    - if [ "${PYTHON}" != 3.4 ]; then
-        pip install -q nose-faulthandler;
-      fi
 
     # Install PyOpenGL
     - if [ "${DEPS}" == "full" ]; then
@@ -104,7 +112,7 @@ install:
           pip install -q pyglet;
           conda install --yes --quiet wxpython > ${REDIRECT_TO};
         else
-          pip install -q http://pyglet.googlecode.com/archive/tip.zip;
+          pip install -q https://bitbucket.org/pyglet/pyglet/get/tip.zip;
         fi;
         if [ "${PYTHON}" == "3.4" ]; then
           conda install --yes --quiet pillow scipy matplotlib > ${REDIRECT_TO};
@@ -121,6 +129,7 @@ install:
     - cd ~
 
     # GLFW: version 2 shouldn't work (so let's try on Py2.6), version 3 will
+    # We can't use most recent builds because our CMake is too old :(
     - if [ "${PYTHON}" == "2.6" ] && [ "${DEPS}" == "full" ]; then
         travis_retry sudo apt-get -qq install libglfw2 > ${REDIRECT_TO};
       fi
@@ -128,6 +137,7 @@ install:
         travis_retry sudo apt-get -qq install xorg-dev libglu1-mesa-dev > ${REDIRECT_TO};
         git clone git://github.com/glfw/glfw.git &> ${REDIRECT_TO};
         cd glfw;
+        git checkout 5b6e671;
         cmake -DBUILD_SHARED_LIBS=true -DGLFW_BUILD_EXAMPLES=false -DGLFW_BUILD_TESTS=false -DGLFW_BUILD_DOCS=false . > ${REDIRECT_TO};
         sudo make install > ${REDIRECT_TO};
       fi
@@ -161,21 +171,29 @@ before_script:
 
 script:
     - cd ${SRC_DIR}
-    - if [ "${TEST}" != "extra" ]; then
-        make nose_coverage;
+    - if [ "${TEST}" == "standard" ]; then
+        make unit;
+      fi;
+    - if [ "${TEST}" == "examples" ] || [ "${DEPS}" == "minimal" ]; then
+        make examples;
       fi;
-    # Require strict adherence to PEP8 and pyflakes (can use "# noqa" to skip)
     - if [ "${TEST}" == "extra" ]; then
         make extra;
       fi;
+    # Each line must be run in a separate line to ensure exit code accuracy
     - if [ "${TEST}" == "extra" ]; then
         echo "Size difference ${SIZE_DIFF} kB";
-        test ${SIZE_DIFF} -lt 100;
+        if git log --format=%B -n 2 | grep -q "\[size skip\]"; then
+          echo "Skipping size test";
+        else
+          test ${SIZE_DIFF} -lt 100;
+        fi;
       fi;
 
 
 after_success:
     # Need to run from source dir to execute appropriate "git" commands
-    - if [ "${TEST}" != "extra" ]; then
+    - if [ "${TEST}" == "standard" ]; then
+        coverage combine;
         coveralls;
       fi;
diff --git a/Makefile b/Makefile
index 12fbd7a..a1fa756 100755
--- a/Makefile
+++ b/Makefile
@@ -30,15 +30,13 @@ in: inplace # just a shortcut
 inplace:
 	python setup.py build_ext -i
 
-nosetests: nose # alias
-
 # Test conditions, don't "clean-so" or builds won't work!
 
-nose: clean-test
-	python make test nose
+unit: clean-test
+	python make test unit
 
-nose_coverage: clean-test
-	python make test nose 1
+examples: clean-test
+	python make test examples
 
 coverage_html:
 	python make coverage_html
@@ -88,8 +86,5 @@ wx: clean-test
 egl: clean-test
 	python make test egl
 
-glut: clean-test
-	python make test glut
-
 ipynb_vnc: clean-test
 	python make test ipynb_vnc
diff --git a/README.rst b/README.rst
index 6fe9ba6..c5cc068 100644
--- a/README.rst
+++ b/README.rst
@@ -3,7 +3,7 @@ Vispy: interactive scientific visualization in Python
 
 Main website: http://vispy.org
 
-|Build Status| |Coverage Status|
+|Build Status| |Appveyor Status| |Coverage Status| |Zenodo Link|
 
 ----
 
@@ -24,6 +24,7 @@ large datasets. Applications of Vispy include:
 Announcements
 -------------
 
+- `Vispy tutorial in the IPython Cookbook <http://ipython-books.github.io/featured-06/>`__
 - **Release!** Version 0.3, August 29, 2014
 - **EuroSciPy 2014**: talk at Saturday 30, and sprint at Sunday 31, August 2014
 - `Article in Linux Magazine, French Edition <https://github.com/vispy/linuxmag-article>`__, July 2014
@@ -62,13 +63,17 @@ Installation
 ------------
 
 Vispy runs on Python 2.6+ and Python 3.3+ and depends on NumPy. You also
-need a backend (PyQt4/PySide, glfw, GLUT, pyglet, or SDL).
+need a backend (PyQt4/PySide, glfw, pyglet, SDL, or wx).
 
 As Vispy is under heavy development at this time, we highly recommend
 you to use the development version on Github (master branch). You need
 to clone the repository and install Vispy with
 ``python setup.py install``.
 
+If you need to install Python for the first time, consider using the
+`Anaconda <http://continuum.io/downloads>`_ Python distribution. It
+provides a convenient package management system.
+
 
 Structure of Vispy
 ------------------
@@ -76,7 +81,7 @@ Structure of Vispy
 Currently, the main subpackages are:
 
 -  **app**: integrates an event system and offers a unified interface on
-   top of many window backends (Qt4, wx, glfw, GLUT, IPython notebook
+   top of many window backends (Qt4, wx, glfw, IPython notebook
    with/without WebGL, and others). Relatively stable API.
 -  **gloo**: a Pythonic, object-oriented interface to OpenGL. Relatively
    stable API.
@@ -133,5 +138,9 @@ External links
 
 .. |Build Status| image:: https://travis-ci.org/vispy/vispy.png?branch=master
    :target: https://travis-ci.org/vispy/vispy
+.. |Appveyor Status| image:: https://ci.appveyor.com/api/projects/status/dsxgkrbfj29xf9ef/branch/master
+   :target: https://ci.appveyor.com/project/Eric89GXL/vispy/branch/master
 .. |Coverage Status| image:: https://coveralls.io/repos/vispy/vispy/badge.png?branch=master
    :target: https://coveralls.io/r/vispy/vispy?branch=master
+.. |Zenodo Link| image:: https://zenodo.org/badge/5822/vispy/vispy.png
+   :target: http://dx.doi.org/10.5281/zenodo.11532
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..1cfa44d
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,36 @@
+# CI on Windows via appveyor
+# This file was based on Olivier Grisel's python-appveyor-demo
+
+environment:
+
+  matrix:
+    - PYTHON: "C:\\Python34-conda64"
+      PYTHON_VERSION: "3.4"
+      PYTHON_ARCH: "64"
+
+install:
+  # Install miniconda Python
+  - "powershell ./make/install_python.ps1"
+
+  # Prepend newly installed Python to the PATH of this build (this cannot be
+  # done from inside the powershell script as it would require to restart
+  # the parent CMD process).
+  - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
+
+  # Check that we have the expected version and architecture for Python
+  - "python --version"
+  - "python -c \"import struct; print(struct.calcsize('P') * 8)\""
+  # Skip using MESA for now since AppVeyor is unreliable with it
+  # "SET VISPY_GL_LIB=%CD%\\opengl32.dll"
+
+  # Install the dependencies of the project.
+  # "conda install --yes --quiet numpy scipy matplotlib setuptools flake8 pyqt nose pytest coverage"
+  - "conda install --yes --quiet numpy scipy matplotlib setuptools nose pytest coverage"
+  - "pip install -q pytest-cov"
+  - "python setup.py develop"
+
+build: false  # Not a C# project, build stuff at the test step instead.
+
+test_script:
+  # Run the project tests
+  - "python make test nobackend"
diff --git a/codegen/annotations.py b/codegen/annotations.py
index 1a31617..ec57b07 100644
--- a/codegen/annotations.py
+++ b/codegen/annotations.py
@@ -17,7 +17,7 @@ import ctypes
 ## bind / gen / delete stuff
 
 def deleteBuffer(buffer):
-    # --- desktop angle
+    # --- gl es
     n = 1  
     buffers = (ctypes.c_uint*n)(buffer)  
     ()  
@@ -25,7 +25,7 @@ def deleteBuffer(buffer):
     GL.glDeleteBuffers(1, [buffer])
 
 def deleteFramebuffer(framebuffer):
-    # --- desktop angle
+    # --- gl es
     n = 1  
     framebuffers = (ctypes.c_uint*n)(framebuffer)  
     ()
@@ -33,7 +33,7 @@ def deleteFramebuffer(framebuffer):
     FBO.glDeleteFramebuffers(1, [framebuffer])
 
 def deleteRenderbuffer(renderbuffer):
-    # --- desktop angle
+    # --- gl es
     n = 1  
     renderbuffers = (ctypes.c_uint*n)(renderbuffer)  
     ()
@@ -41,7 +41,7 @@ def deleteRenderbuffer(renderbuffer):
     FBO.glDeleteRenderbuffers(1, [renderbuffer])
 
 def deleteTexture(texture):
-    # --- desktop angle
+    # --- gl es
     n = 1  
     textures = (ctypes.c_uint*n)(texture)  
     ()
@@ -50,7 +50,7 @@ def deleteTexture(texture):
 
 
 def createBuffer():
-    # --- desktop angle
+    # --- gl es
     n = 1
     buffers = (ctypes.c_uint*n)()
     ()  
@@ -61,7 +61,7 @@ def createBuffer():
     return 1
 
 def createFramebuffer():
-    # --- desktop angle
+    # --- gl es
     n = 1
     framebuffers = (ctypes.c_uint*n)()
     ()
@@ -72,7 +72,7 @@ def createFramebuffer():
     return 1
 
 def createRenderbuffer():
-    # --- desktop angle
+    # --- gl es
     n = 1
     renderbuffers = (ctypes.c_uint*n)()
     ()
@@ -83,7 +83,7 @@ def createRenderbuffer():
     return 1
 
 def createTexture():
-    # --- desktop angle
+    # --- gl es
     n = 1
     textures = (ctypes.c_uint*n)()
     ()
@@ -98,7 +98,7 @@ def createTexture():
 
 def texImage2D(target, level, internalformat, format, type, pixels):
     border = 0
-    # --- desktop angle
+    # --- gl es
     if isinstance(pixels, (tuple, list)):
         height, width = pixels
         pixels = ctypes.c_void_p(0)
@@ -121,7 +121,7 @@ def texImage2D(target, level, internalformat, format, type, pixels):
 
 
 def texSubImage2D(target, level, xoffset, yoffset, format, type, pixels):
-    # --- desktop angle
+    # --- gl es
     if not pixels.flags['C_CONTIGUOUS']:
         pixels = pixels.copy('C')
     pixels_ = pixels
@@ -134,12 +134,13 @@ def texSubImage2D(target, level, xoffset, yoffset, format, type, pixels):
 
 
 def readPixels(x, y, width, height, format, type):
-    # --- desktop angle mock
+    # --- gl es mock
     # GL_ALPHA, GL_RGB, GL_RGBA
     t = {6406:1, 6407:3, 6408:4}[format]
-    # we kind of only support type GL_UNSIGNED_BYTE
-    size = int(width*height*t)
-    # --- desktop angle
+    # GL_UNSIGNED_BYTE, GL_FLOAT
+    nb = {5121:1, 5126:4}[type]
+    size = int(width*height*t*nb)
+    # --- gl es
     pixels = ctypes.create_string_buffer(size)
     ()
     return pixels[:]
@@ -149,7 +150,7 @@ def readPixels(x, y, width, height, format, type):
 
 def compressedTexImage2D(target, level, internalformat, width, height, border=0, data=None):
     # border = 0  # set in args
-    # --- desktop angle
+    # --- gl es
     if not data.flags['C_CONTIGUOUS']:
         data = data.copy('C')
     data_ = data
@@ -162,7 +163,7 @@ def compressedTexImage2D(target, level, internalformat, width, height, border=0,
 
 
 def compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, data):
-    # --- desktop angle
+    # --- gl es
     if not data.flags['C_CONTIGUOUS']:
         data = data.copy('C')
     data_ = data
@@ -180,7 +181,7 @@ def compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, form
 def bufferData(target, data, usage):
     """ Data can be numpy array or the size of data to allocate.
     """
-    # --- desktop angle
+    # --- gl es
     if isinstance(data, int):
         size = data
         data = ctypes.c_voidp(0)
@@ -201,7 +202,7 @@ def bufferData(target, data, usage):
 
 
 def bufferSubData(target, offset, data):
-    # --- desktop angle
+    # --- gl es
     if not data.flags['C_CONTIGUOUS']:
         data = data.copy('C')
     data_ = data
@@ -214,7 +215,7 @@ def bufferSubData(target, offset, data):
 
 
 def drawElements(mode, count, type, offset):
-    # --- desktop angle
+    # --- gl es
     if offset is None:
         offset = ctypes.c_void_p(0)
     elif isinstance(offset, ctypes.c_void_p):
@@ -237,7 +238,7 @@ def drawElements(mode, count, type, offset):
 
 
 def vertexAttribPointer(indx, size, type, normalized, stride, offset):
-    # --- desktop angle
+    # --- gl es
     if offset is None:
         offset = ctypes.c_void_p(0)
     elif isinstance(offset, ctypes.c_void_p):
@@ -264,7 +265,7 @@ def vertexAttribPointer(indx, size, type, normalized, stride, offset):
 
 
 def bindAttribLocation(program, index, name):
-    # --- desktop angle
+    # --- gl es
     name = ctypes.c_char_p(name.encode('utf-8'))
     ()
     # --- pyopengl
@@ -281,7 +282,7 @@ def shaderSource(shader, source):
         strings = [s for s in source]
     else:
         strings = [source]
-    # --- desktop angle
+    # --- gl es
     count = len(strings)  
     string = (ctypes.c_char_p*count)(*[s.encode('utf-8') for s in strings])  
     length = (ctypes.c_int*count)(*[len(s) for s in strings])  
@@ -293,13 +294,13 @@ def shaderSource(shader, source):
 ## Getters
 
 def _getBooleanv(pname):
-    # --- desktop angle
+    # --- gl es
     params = (ctypes.c_bool*1)()
     ()
     return params[0]
 
 def _getIntegerv(pname):
-    # --- desktop angle
+    # --- gl es
     n = 16
     d = -2**31  # smallest 32bit integer
     params = (ctypes.c_int*n)(*[d for i in range(n)])
@@ -311,7 +312,7 @@ def _getIntegerv(pname):
         return tuple(params)
 
 def _getFloatv(pname):
-    # --- desktop angle
+    # --- gl es
     n = 16
     d = float('Inf')
     params = (ctypes.c_float*n)(*[d for i in range(n)])
@@ -323,7 +324,7 @@ def _getFloatv(pname):
         return tuple(params)
 
 # def _getString(pname):
-#     # --- desktop angle
+#     # --- gl es
 #     ()
 #     return res.value
 #     # --- mock
@@ -345,16 +346,16 @@ def getParameter(pname):
     else:
         return _glGetIntegerv(pname)
     name = pname
-    # --- desktop angle
+    # --- gl es
     ()
-    return res.decode('utf-8') if res else ''
+    return ctypes.string_at(res).decode('utf-8') if res else ''
     # --- pyopengl
     res = GL.glGetString(pname)
     return res.decode('utf-8')
 
 
 def getUniform(program, location):
-    # --- desktop angle
+    # --- gl es
     n = 16
     d = float('Inf')
     params = (ctypes.c_float*n)(*[d for i in range(n)])
@@ -377,7 +378,7 @@ def getUniform(program, location):
 
 
 def getVertexAttrib(index, pname):
-    # --- desktop angle
+    # --- gl es
     n = 4
     d = float('Inf')
     params = (ctypes.c_float*n)(*[d for i in range(n)])
@@ -404,7 +405,7 @@ def getVertexAttrib(index, pname):
 
 
 def getTexParameter(target, pname):
-    # --- desktop angle
+    # --- gl es
     d = float('Inf')
     params = (ctypes.c_float*1)(d)
     ()
@@ -412,13 +413,13 @@ def getTexParameter(target, pname):
 
 
 def getActiveAttrib(program, index):
-    # --- desktop angle pyopengl
+    # --- gl es pyopengl
     bufsize = 256
     length = (ctypes.c_int*1)()
     size = (ctypes.c_int*1)()
     type = (ctypes.c_uint*1)()
     name = ctypes.create_string_buffer(bufsize)
-    # --- desktop angle
+    # --- gl es
     ()
     name = name[:length[0]].decode('utf-8')
     return name, size[0], type[0]
@@ -432,7 +433,7 @@ def getActiveAttrib(program, index):
 
 
 def getVertexAttribOffset(index, pname):
-    # --- desktop angle
+    # --- gl es
     pointer = (ctypes.c_void_p*1)()
     ()
     return pointer[0] or 0
@@ -448,7 +449,7 @@ def getVertexAttribOffset(index, pname):
 
     
 def getActiveUniform(program, index):
-    # --- desktop angle
+    # --- gl es
     bufsize = 256
     length = (ctypes.c_int*1)()
     size = (ctypes.c_int*1)()
@@ -463,7 +464,7 @@ def getActiveUniform(program, index):
 
 
 def getAttachedShaders(program):
-    # --- desktop angle
+    # --- gl es
     maxcount = 256
     count = (ctypes.c_int*1)()
     shaders = (ctypes.c_uint*maxcount)()
@@ -472,7 +473,7 @@ def getAttachedShaders(program):
 
 
 def getAttribLocation(program, name):
-    # --- desktop angle
+    # --- gl es
     name = ctypes.c_char_p(name.encode('utf-8'))
     ()
     return res
@@ -482,7 +483,7 @@ def getAttribLocation(program, name):
     
 
 def getUniformLocation(program, name):
-    # --- desktop angle
+    # --- gl es
     name = ctypes.c_char_p(name.encode('utf-8'))
     ()
     return res
@@ -491,7 +492,7 @@ def getUniformLocation(program, name):
     ()
 
 def getProgramInfoLog(program):
-    # --- desktop angle
+    # --- gl es
     bufsize = 1024
     length = (ctypes.c_int*1)()
     infolog = ctypes.create_string_buffer(bufsize)
@@ -499,10 +500,10 @@ def getProgramInfoLog(program):
     return infolog[:length[0]].decode('utf-8')
     # --- pyopengl
     res = GL.glGetProgramInfoLog(program)
-    return res.decode('utf-8')
+    return res.decode('utf-8') if isinstance(res, bytes) else res
 
 def getShaderInfoLog(shader):
-    # --- desktop angle
+    # --- gl es
     bufsize = 1024
     length = (ctypes.c_int*1)()
     infolog = ctypes.create_string_buffer(bufsize)
@@ -510,29 +511,29 @@ def getShaderInfoLog(shader):
     return infolog[:length[0]].decode('utf-8')
     # --- pyopengl
     res = GL.glGetShaderInfoLog(shader)
-    return res.decode('utf-8')
+    return res.decode('utf-8') if isinstance(res, bytes) else res
 
 def getProgramParameter(program, pname):
-    # --- desktop angle
+    # --- gl es
     params = (ctypes.c_int*1)()
     ()
     return params[0]
 
 def getShaderParameter(shader, pname):
-    # --- desktop angle
+    # --- gl es
     params = (ctypes.c_int*1)()
     ()
     return params[0]
 
 def getShaderPrecisionFormat(shadertype, precisiontype):
-    # --- desktop angle
+    # --- gl es
     range = (ctypes.c_int*1)()
     precision = (ctypes.c_int*1)()
     ()
     return range[0], precision[0]
 
 def getShaderSource(shader):
-    # --- desktop angle
+    # --- gl es
     bufsize = 1024*1024
     length = (ctypes.c_int*1)()
     source = (ctypes.c_char*bufsize)()
@@ -544,7 +545,7 @@ def getShaderSource(shader):
     
 
 def getBufferParameter(target, pname):
-    # --- desktop angle
+    # --- gl es
     d = -2**31  # smallest 32bit integer
     params = (ctypes.c_int*1)(d)
     ()
@@ -552,7 +553,7 @@ def getBufferParameter(target, pname):
 
 
 def getFramebufferAttachmentParameter(target, attachment, pname):
-    # --- desktop angle
+    # --- gl es
     d = -2**31  # smallest 32bit integer
     params = (ctypes.c_int*1)(d)
     ()
@@ -565,7 +566,7 @@ def getFramebufferAttachmentParameter(target, attachment, pname):
 
 
 def getRenderbufferParameter(target, pname):
-    # --- desktop angle
+    # --- gl es
     d = -2**31  # smallest 32bit integer
     params = (ctypes.c_int*1)(d)
     ()
@@ -595,11 +596,11 @@ class FunctionAnnotation:
         """ Get the lines for this function based on the given backend. 
         The given API call is inserted at the correct location.
         """
-        backend_selector = backend  # first lines are for all backends
+        backend_selector = (backend, )  # first lines are for all backends
         lines = []
         for line in self.lines:
             if line.lstrip().startswith('# ---'):
-                backend_selector = line
+                backend_selector = line.strip().split(' ')
                 continue
             if backend in backend_selector:
                 if line.strip() == '()':
diff --git a/codegen/createglapi.py b/codegen/createglapi.py
index f08be8d..3f5dcda 100644
--- a/codegen/createglapi.py
+++ b/codegen/createglapi.py
@@ -24,8 +24,9 @@ Some groups of functions (like glUniform and friends) are handled in
 *this* file.
 
 Even though the API is quite small, we want to generate several
-implementations, such as desktop, Angle, a generic proxy, webgl, a mock
-backend and possibly more. Therefore automation is crucial.
+implementations, such as gl2 (desktop), es2 (angle on Windows), a generic
+proxy, a mock backend and possibly more. Therefore automation
+is crucial.
 
 Further notes
 -------------
@@ -35,10 +36,10 @@ as possible, it's not always that easy to read. In effect this code is
 not so easy to maintain. I hope it is at least clear enough so it can be
 used to maintain the GL API itself.
 
+This function must be run using Python3.
 """
 
 import os
-import sys
 import ctypes  # not actually used, but handy to have imported during dev
 
 import headerparser
@@ -60,7 +61,7 @@ THIS CODE IS AUTO-GENERATED. DO NOT EDIT.
 
 ## Create parsers
 
-# Create a parser for desktop and web gl
+# Create a parser for gl2 and es2
 parser1 = headerparser.Parser(os.path.join(THISDIR, 'headers', 'gl2.h'))
 headerparser.CONSTANTS = {}
 parser2 = headerparser.Parser(os.path.join(THISDIR, 'headers', 'webgl.idl'))
@@ -469,10 +470,6 @@ class ProxyApiGenerator(ApiGenerator):
        
         def __call__(self, funcname, returns, *args):
             raise NotImplementedError()
-        
-        
-        def glShaderSource_compat(self, handle, code):
-            return self("glShaderSource_compat", True, handle, code)
     '''
     
     def _returns(self, des):
@@ -504,18 +501,18 @@ class ProxyApiGenerator(ApiGenerator):
 
 
 
-class DesktopApiGenerator(ApiGenerator):
-    """ Generator for the desktop GL backend.
+class Gl2ApiGenerator(ApiGenerator):
+    """ Generator for the gl2 (desktop) backend.
     """
     
-    filename = os.path.join(GLDIR, '_desktop.py')
+    filename = os.path.join(GLDIR, '_gl2.py')
     write_c_sig = True
     define_argtypes_in_module = False
     
     DESCRIPTION = "Subset of desktop GL API compatible with GL ES 2.0"
     PREAMBLE = """
     import ctypes
-    from .desktop import _lib, _get_gl_func
+    from .gl2 import _lib, _get_gl_func
     """
     
     def _get_argtype_str(self, es2func):
@@ -609,7 +606,7 @@ class DesktopApiGenerator(ApiGenerator):
             # Annotation available
             functions_anno.add(des.name)
             callline = self._native_call_line(des.name, es2func, prefix=prefix)
-            lines.extend( des.ann.get_lines(callline, 'desktop') )
+            lines.extend( des.ann.get_lines(callline, 'gl') )
         
         elif es2func.group:
             # Group?
@@ -635,8 +632,8 @@ class DesktopApiGenerator(ApiGenerator):
             lines.append(callline)
         
         
-        if 'desktop' in self.__class__.__name__.lower():
-            # Post-fix special cases for desktop gl. See discussion in #201
+        if 'gl2' in self.__class__.__name__.lower():
+            # Post-fix special cases for gl2. See discussion in #201
             # glDepthRangef and glClearDepthf are not always available,
             # and sometimes they do not work if they are
             if es2func.oname in ('glDepthRangef', 'glClearDepthf'):
@@ -692,20 +689,20 @@ class DesktopApiGenerator(ApiGenerator):
             raise ValueError('unknown group func')
 
 
-class AngleApiGenrator(DesktopApiGenerator):
-    """ Generator for the Angle backend (GL to Directx conversion on
-    Windows). Very similar to the desktop API, but we do not need that
-    deferred loading of GL functions here.
+class Es2ApiGenrator(Gl2ApiGenerator):
+    """ Generator for the es2 backend (i.e. Angle on Windows). Very
+    similar to the gl2 API, but we do not need that deferred loading
+    of GL functions here.
     """
     
-    filename = os.path.join(GLDIR, '_angle.py')
+    filename = os.path.join(GLDIR, '_es2.py')
     write_c_sig = True
     define_argtypes_in_module = True
     
-    DESCRIPTION = "GL ES 2.0 API based on the Angle library (i.e. DirectX)"
+    DESCRIPTION = "GL ES 2.0 API (via Angle/DirectX on Windows)"
     PREAMBLE = """
     import ctypes
-    from .angle import _lib
+    from .es2 import _lib
     """
     
     def _native_call_line(self, name, es2func, cargstr=None, prefix='', indent=4):
@@ -717,11 +714,11 @@ class AngleApiGenrator(DesktopApiGenerator):
 
 
 
-class PyOpenGLApiGenrator(ApiGenerator):
+class PyOpenGL2ApiGenrator(ApiGenerator):
     """ Generator for a fallback pyopengl backend.
     """
     
-    filename = os.path.join(GLDIR, '_pyopengl.py')
+    filename = os.path.join(GLDIR, '_pyopengl2.py')
     DESCRIPTION = 'Proxy API for GL ES 2.0 subset, via the PyOpenGL library.'
     PREAMBLE = """
     import ctypes
@@ -732,6 +729,7 @@ class PyOpenGLApiGenrator(ApiGenerator):
     def __init__(self):
         ApiGenerator.__init__(self)
         self._functions_to_import = []
+        self._used_functions = []
 
     def _add_function(self, des):
         # Fix for FBO?
@@ -749,6 +747,7 @@ class PyOpenGLApiGenrator(ApiGenerator):
         if ann_lines:
             self.lines.append('def %s(%s):' % (des.apiname, argstr))
             self.lines.extend(ann_lines)
+            self._used_functions.append(des.es2.glname)
         else:
             # To be imported from OpenGL.GL
             self._functions_to_import.append((des.es2.glname, des.apiname))
@@ -766,6 +765,15 @@ class PyOpenGLApiGenrator(ApiGenerator):
             self.lines.append('    ("%s", "%s"),' % (name1, name2))
         self.lines.append('    ]')
         
+        self.lines.append('')
+        
+        # Write used functions
+        self.lines.append('# List of functions in OpenGL.GL that we use')
+        self.lines.append('_used_functions = [')
+        for name in self._used_functions:
+            self.lines.append('    "%s",' % name)
+        self.lines.append('    ]')
+        
         # Really save
         ApiGenerator.save(self)
 
@@ -773,8 +781,8 @@ class PyOpenGLApiGenrator(ApiGenerator):
 ## Generate
 
 # Generate
-for Gen in [ProxyApiGenerator, DesktopApiGenerator, AngleApiGenrator, 
-            PyOpenGLApiGenrator]:
+for Gen in [ProxyApiGenerator, Gl2ApiGenerator, Es2ApiGenrator, 
+            PyOpenGL2ApiGenrator]:
     gen = Gen()
     for des in functions:
         gen.add_function(des)
diff --git a/codegen/get-deprecated.py b/codegen/get-deprecated.py
new file mode 100644
index 0000000..955e896
--- /dev/null
+++ b/codegen/get-deprecated.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+""" Get deprecated funcsions by parsing gl.spec.
+
+gl.spec is not included in the repo for reasons of space. But you can
+easily download it.
+"""
+
+import os
+import sys
+
+THISDIR = os.path.abspath(os.path.dirname(__file__))
+
+# Load text
+filename = os.path.join(THISDIR, 'headers', 'gl.spec')
+text = open(filename, 'rb').read().decode('utf-8')
+
+# Define chars that a function name must begin with
+chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+assert len(chars) == 26
+
+
+# Get deprecated functions
+
+deprecated = set()
+
+currentFunc = None
+for line in text.splitlines():
+    if line.endswith(')') and '(' in line and line[0] in chars:
+        currentFunc = line.split('(')[0]
+    
+    elif not currentFunc:
+        pass
+    
+    elif line.startswith('\t'):
+        line = line.replace('\t', ' ')
+        parts = line.split(' ')
+        parts = [part.strip() for part in parts]
+        parts = [part for part in parts if part]
+        key = parts[0]
+        if len(parts) > 1 :
+            val, comment = parts[1], parts[2:]
+        if key == 'deprecated' and float(val) <= 3.1:
+            deprecated.add(currentFunc)
+
+
+assert 'Begin' in deprecated
+
+
+# Print
+
+print('='*80)
+
+pendingline = '    '
+for name in sorted(deprecated):
+    name = 'gl' + name
+    if len(pendingline) + len(name) < 77:
+        pendingline += name + ', '
+    else:
+        print(pendingline)
+        pendingline = '    ' + name + ', '
+print(pendingline)
+print('='*80)
+
+# Report
+print('Found %i deprecated functions' % len(deprecated) )
diff --git a/codegen/headerparser.py b/codegen/headerparser.py
index 3c74d0b..6b2c970 100644
--- a/codegen/headerparser.py
+++ b/codegen/headerparser.py
@@ -227,7 +227,7 @@ class ConstantDefinition(Definition):
         """
         self.value = None
         line = line.split('/*', 1)[0]
-        _, *args = getwords(line)
+        args = getwords(line)[1:]
         self.isvalid = False
         if len(args) == 1:
             pass
@@ -278,7 +278,8 @@ class FunctionDefinition(Definition):
         # Parse components
         beforeBrace, args = line.split('(', 1)
         betweenBraces, _ = args.split(')', 1)
-        *prefix, name = getwords(beforeBrace)
+        outs = getwords(beforeBrace)
+        prefix, name = outs[:-1], outs[-1]
 
         # Store name
         self._set_name(name)
diff --git a/doc/ext/gloooverviewgenerator.py b/doc/ext/gloooverviewgenerator.py
index 599267b..7bee591 100644
--- a/doc/ext/gloooverviewgenerator.py
+++ b/doc/ext/gloooverviewgenerator.py
@@ -12,6 +12,10 @@ def main():
 def clean():
     pass
 
+EXCLUDE = ['ColorBuffer', 'DepthBuffer', 'StencilBuffer']
+CLASSES = ['Program', 'VertexBuffer', 'IndexBuffer', 'Texture1D', 'Texture2D', 'Texture3D',
+           'RenderBuffer', 'FrameBuffer']
+
 
 def get_docs_for_class(klass):
     """ Get props and methods for a class.
@@ -19,13 +23,11 @@ def get_docs_for_class(klass):
 
     # Prepare
     baseatts = dir(gloo.GLObject)
-    functype = type(gloo.GLObject.activate)
-    proptype = type(gloo.GLObject.handle)
+    functype = type(gloo.GLObject.delete)
+    proptype = type(gloo.GLObject.id)
     props, funcs = set(), set()
 
     for att in sorted(dir(klass)):
-        if klass is not gloo.GLObject and att in baseatts:
-            continue
         if att.startswith('_') or att.lower() != att:
             continue
         # Get ob and module name
@@ -41,6 +43,8 @@ def get_docs_for_class(klass):
                 break
         if actualklass == klass:
             modulename = ''
+        elif actualklass is gloo.GLObject:
+            modulename = gloo.GLObject.__module__.split('.')[-1]
         # Append
         if isinstance(attob, functype):
             funcs.add(' :meth:`~%s.%s.%s`,' % (
@@ -57,9 +61,9 @@ def generate_overview_docs():
     """
 
     lines = []
-    lines.append('Overview')
+    lines.append('Overview of most important GLOO classes')
     lines.append('=' * len(lines[-1]))
-    klasseses = ((getattr(gloo, d),) for d in dir(gloo) if d[0].isupper())
+    klasseses = [(getattr(gloo, d),) for d in CLASSES]
     for klasses in klasseses:
         # Init line
         line = '*'
@@ -85,3 +89,7 @@ def generate_overview_docs():
             lines.append(line[:-1])
 
     return '\n'.join(lines)
+
+
+if __name__ == '__main__':
+    print(generate_overview_docs())
diff --git a/doc/gloo.rst b/doc/gloo.rst
index d879738..5d351bb 100644
--- a/doc/gloo.rst
+++ b/doc/gloo.rst
@@ -12,51 +12,50 @@ Base class
     :members:
 
 
-Classes related to shaders
+Program class
 ==========================
 
 .. autoclass:: vispy.gloo.Program
     :members:
 
 
-.. autoclass:: vispy.gloo.VertexShader
-    :members:
-
-.. autoclass:: vispy.gloo.FragmentShader
-    :members:
-
-.. autoclass:: vispy.gloo.shader.Shader
-    :members:
-
-
 Buffer classes
 ==============
 
-.. autoclass:: vispy.gloo.VertexBuffer
+.. autoclass:: vispy.gloo.buffer.Buffer
     :members:
 
-.. autoclass:: vispy.gloo.IndexBuffer
+.. autoclass:: vispy.gloo.buffer.DataBuffer
     :members:
 
-.. autoclass:: vispy.gloo.buffer.DataBuffer
+.. autoclass:: vispy.gloo.VertexBuffer
     :members:
 
-.. autoclass:: vispy.gloo.buffer.Buffer
+.. autoclass:: vispy.gloo.IndexBuffer
     :members:
 
 
 Texture classes
 ===============
 
+.. autoclass:: vispy.gloo.texture.BaseTexture
+    :members:
+
 .. autoclass:: vispy.gloo.Texture2D
+    :members:
 
 .. autoclass:: vispy.gloo.Texture3D
+    :members:
 
 .. autoclass:: vispy.gloo.TextureAtlas
+    :members:
 
 
-Classes related to FBO
-======================
+Classes related to FBO's
+========================
+
+.. autoclass:: vispy.gloo.RenderBuffer
+    :members:
 
 .. autoclass:: vispy.gloo.FrameBuffer
     :members:
@@ -69,6 +68,13 @@ State methods
     :members:
 
 
+The OpenGL context 
+==================
+
+.. automodule:: vispy.gloo.context
+    :members:
+
+
 vispy.gloo.gl - low level GL API
 ================================
 
diff --git a/doc/index.rst b/doc/index.rst
index 5326967..c1216a8 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -6,9 +6,13 @@ library**. Vispy leverages the computational power of modern **Graphics
 Processing Units (GPUs)** through the **OpenGL** library to display very
 large datasets.
 
+.. toctree::
+   :maxdepth: 1
+
+   index
 
 Overview
---------
+========
 
 Vispy is a young library under heavy development at this time. It
 targets two categories of users:
@@ -37,11 +41,12 @@ We are still working on a complete user guide for Vispy. In the meantime, you
 can:
 
   * Check out the `gallery <http://vispy.org/gallery.html>`_
-  * Use the ``mpl_plot`` experimental OpenGL backend for matplotlib
+  * Use the ``vispy.plot`` and ``vispy.scene`` interfaces for high-level work
+    (WARNING: experimental / developing code)
+  * Use the ``vispy.mpl_plot`` experimental OpenGL backend for matplotlib
   * Start learning OpenGL (see below)
-  * Write your own visualizations with **gloo** (require knowing some 
+  * Write your own visualizations with ``vispy.gloo`` (requires knowing some 
     OpenGL/GLSL)
-  * Start using the higher-level interfaces (visuals, scene graph)
   
 
 Learning the fundamentals of modern OpenGL
@@ -55,7 +60,7 @@ Even when Vispy is mature enough, knowing OpenGL will still let you write
 entirely custom interactive visualizations that fully leverage the power of
 GPUs.
 
-  * `A tutorial about Vispy <http://ipython-books.github.io/featured-06.html>`_, by Cyrille Rossant, published in the `IPython Cookbook <http://ipython-books.github.io/>`_
+  * `A tutorial about Vispy <http://ipython-books.github.io/featured-06/>`_, by Cyrille Rossant, published in the `IPython Cookbook <http://ipython-books.github.io/>`_
   * `A tutorial about modern OpenGL and Vispy, by Nicolas Rougier <http://www.loria.fr/~rougier/teaching/opengl/>`_
   * A paper on the fundamentals behing Vispy: `Rossant C and Harris KD, Hardware-accelerated interactive data visualization for neuroscience in Python, Frontiers in Neuroinformatics 2013 <http://journal.frontiersin.org/Journal/10.3389/fninf.2013.00036/full>`_
   * A free online book on modern OpenGL (but not Python): `Learning Modern 3D Graphics Programming, by Jason L. McKesson <http://www.arcsynthesis.org/gltut/>`_
@@ -64,7 +69,7 @@ GPUs.
 
 
 API reference
--------------
+=============
 
 .. toctree::
    :maxdepth: 2
@@ -76,7 +81,8 @@ API reference
    vispy.gloo - User-friendly, Pythonic, object-oriented interface to OpenGL <gloo>
    vispy.io - Data IO <io>
    vispy.mpl_plot - OpenGL backend for matplotlib [experimental] <mpl_plot>
-   vispy.scene - The system underlying the upcoming high-level visualization interfaces [highly experimental] <scene>
+   vispy.plot - Vispy native plotting module <plot>
+   vispy.scene - The system underlying the upcoming high-level visualization interfaces [experimental] <scene>
    vispy.util - Miscellaneous utilities <util>
    
    examples
diff --git a/doc/scene.rst b/doc/scene.rst
index 0879389..ad6c63a 100644
--- a/doc/scene.rst
+++ b/doc/scene.rst
@@ -1,6 +1,6 @@
-===========
-Scene graph
-===========
+==========
+Scenegraph
+==========
 
 .. automodule:: vispy.scene
    :members:
@@ -15,17 +15,25 @@ vispy.scene.cameras
 
 ----
 
-vispy.scene.shaders
+vispy.scene.canvas
 -------------------
-.. automodule:: vispy.scene.shaders
+.. automodule:: vispy.scene.canvas
    :members:
 
 
 ----
 
-vispy.scene.transforms
-----------------------
-.. automodule:: vispy.scene.transforms
+vispy.scene.node
+----------------
+.. automodule:: vispy.scene.node
+   :members:
+
+
+----
+
+vispy.scene.events
+------------------
+.. automodule:: vispy.scene.events
    :members:
 
 
diff --git a/doc/user_guide.md b/doc/user_guide.md
new file mode 100644
index 0000000..b982063
--- /dev/null
+++ b/doc/user_guide.md
@@ -0,0 +1,329 @@
+# VisPy user guide
+
+Welcome to the VisPy user guide!
+
+## Introduction to VisPy
+
+VisPy aims at offering high-quality, high-performance scientific visualization facilities in Python. In development since 2013, VisPy represents a joint effort of four developers who had independently worked on OpenGL-based visualization libraries. With the low-level foundations being layed out, the plotting API of VisPy is now usable by all scientists. No knowledge of OpenGL is required for most plotting use-cases.
+
+There are three levels with which VisPy can be used:
+
+* 3. **Scientific plotting interface**: most use-cases
+* 2. **Scene graph**: advanced use-cases
+* 1. **Pythonic OpenGL (gloo)**: optimal performance on highly-specific use-cases
+
+Level 3 is implemented on top of level 2 which is itself implemented on top of level 1.
+
+
+## Interactive scientific plots
+
+Several common plotting functions are available in `vispy.plot`. The plotting interface is still in extensive development and the API might be subject to changes in future versions.
+
+### Line plot
+
+TODO
+
+```python
+import vispy.plot as vp
+vp.plot(x, y)
+```
+
+### Scatter plot
+
+TODO
+
+
+### Images
+
+TODO
+
+
+### Surface plot
+
+TODO
+
+
+### matplotlib and VisPy
+
+There is an experimental matplotlib-to-VisPy converter in `vispy.mpl_plot`.
+
+```python
+import vispy.mpl_plot as plt
+plt.plot(x, y)  # this is a normal matplotlib API call
+plt.draw()  # replace the normal matplotlib plt.show() by this
+```
+
+It still doesn't work in all cases, but eventually it will.
+
+
+## The scene graph
+
+The scene graph targets users who have more advanced needs than what `vispy.plot` provides. The scene graph is a dynamic graph where nodes are visual elements, and edges represent relative transformations. The scene graph therefore encodes the positions of all visual elements in the scene.
+
+TODO
+
+### Displaying an empty window
+
+* Changing the background color
+
+
+### Displaying visuals
+
+
+
+### Handling transforms
+
+
+## Pythonic OpenGL (gloo)
+
+OpenGL is a low-level graphics API that gives access to the **Graphics Processing Unit (GPU)** for high-performance real-time 2D/3D visualization. It is a complex API, but VisPy implements a user-friendly and Pythonic interface that makes it considerably easier to use OpenGL.
+
+### Creating a window
+
+First, let's see how to create a window:
+
+```python
+from vispy import app, gloo
+
+# Create a canvas with common key bindings for interactive use.
+canvas = app.Canvas(keys='interactive')
+
+# Use this decorator to register event handlers.
+ at canvas.connect
+def on_draw(event):
+    # The window needs to be cleared at every draw.
+    gloo.clear('deepskyblue')
+
+canvas.show()
+app.run()  # Not necessary in interactive mode.
+```
+
+
+### Event handlers
+
+Here are the main events. Every function accepts an `event` object as argument. The attributes of this object depend on the event, you will find them in the API documentation.
+
+* `on_resize`: when the window is resized. Use-cases: set the viewport, reorganize the layout of the visuals.
+* `on_draw`: when the scene is drawn.
+* `on_mouse_press`: when the mouse is pressed. Attributes:
+    * `event.buttons`: list of pressed buttons (1, 2, or 3)
+    * `events.pos` (in window coordinates)
+* `on_mouse_release`: when the mouse is released after it has been pressed.
+* `on_mouse_move`: when the mouse moves. The `event.is_dragging` attribute can be used to handle drag and drop events.
+* `on_mouse_wheel`: when the mouse wheel changes. Attributes:
+    * `event.delta[1]`: amount of scroll in the vertical direction.
+* `on_key_press`: when a key is pressed. Attributes:
+    * `event.key`: `Key` instance.
+    * `event.text`: string with the key pressed.
+* `on_key_release`: when a key is released.
+
+Here is an example:
+
+```python
+import numpy as np
+from vispy import app, gloo
+
+canvas = app.Canvas(keys='interactive')
+
+ at canvas.connect
+def on_mouse_move(event):
+    x, y = event.pos  # Position of the cursor in window coordinates.
+    w, h = canvas.size  # Size of the window (in pixels).
+    
+    # We update the background color when the mouse moves.
+    r, b = np.clip([x / float(w), y / float(h)], 0, 1)
+    gloo.clear((r, 0.0, b, 1.0))
+
+    # This is used to refresh the window and trigger a draw event.
+    canvas.update()
+
+canvas.show()
+app.run()
+```
+
+### Introduction to the rendering pipeline
+
+Now that we can display windows, let's get started with OpenGL.
+
+> This paragraph comes mostly from the [IPython Cookbook](https://ipython-books.github.io/featured-06/).
+
+The **rendering pipeline** defines how data is processed on the GPU for rendering.
+
+There are four major elements in the rendering pipeline of a given OpenGL program:
+
+* **Data buffers** store numerical data on the GPU. The main types of buffers are **vertex buffers**, **index buffers**, and **textures**.
+
+* **Variables** are available in the shaders. There are four major types of variables: **attributes**, **uniforms**, **varyings**, and **texture samplers**.
+
+* **Shaders** are GPU programs written in a C-like language called OpenGL Shading Language (GLSL). The two main types of shaders are **vertex shaders** and **fragment shaders**.
+
+* The **primitive type** defines the way data points are rendered. The main types are **points**, **lines*, and **triangles**.
+
+Here is how the rendering pipeline works:
+
+1. Data is sent on the GPU and stored in buffers.
+2. The vertex shader processes the data in parallel and generates a number of 4D points in a normalized coordinate system (+/-1, +/-1). The fourth dimension is a homogeneous coordinate (generally 1).
+3. Graphics primitives are generated from the data points returned by the vertex shader (primitive assembly and rasterization).
+4. The fragment shader processes all primitive pixels in parallel and returns each pixel's color as RGBA components.
+
+In `vispy.gloo`, a Program is created with the vertex and fragment shaders. Then, the variables declared in the shaders can be set with the syntax `program['varname'] = value`. When `varname` is an attribute variable, the value can just be a NumPy 2D array. In this array, every line contains the components of every data point.
+
+Similarly, we could declare and set uniforms and textures in our program.
+
+Finally, `program.draw()` renders the data using the specified primitive type (point, line, or triangle).
+
+In addition, an index buffer may be provided. An index buffer contains indices pointing to the vertex buffers. Using an index buffer would allow us to reuse any vertex multiple times during the primitive assembly stage. For example, when rendering a cube with a triangles primitive type (one triangle is generated for every triplet of points), we could use a vertex buffer with 8 data points and an index buffer with 36 indices (3 points per triangle, 2 triangles per face, 6 faces).
+
+
+### First GLSL shaders
+
+Let's now write our first shaders. We will just display points in two dimensions. We will need the following components:
+
+* A `(n, 2)` NumPy array with the 2D positions of the `n` points.
+* A simple vertex shader performing no transformation at all on the point coordinates.
+* A simple fragment shader returning a constant pixel color.
+
+There is one attribute variable `a_position`. This `vec2` variable encodes the (x, y) coordinates of every point.
+
+```python
+from vispy import app, gloo
+import numpy as np
+
+vertex_shader = """
+attribute vec2 a_position;
+void main() {
+    // gl_Position is the special return value of the vertex shader.
+    // It is a vec4, so here we convert the vec2 position into 
+    // a vec4. We set the third component to 0 because we're in 2D, and the 
+    // fourth one to 1 (we don't need homogeneous coordinates here).
+    gl_Position = vec4(a_position, 0.0, 1.0);
+
+    // This is the size of the displayed points.
+    gl_PointSize = 1.0;
+}
+"""
+
+fragment_shader = """
+void main() {
+    // gl_FragColor is the special return value of the fragment shader.
+    // It is a vec4 with rgba components. Here we just return the color blue.
+    gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
+}
+"""
+
+# We create an OpenGL program with the vertex and fragment shader.
+program = gloo.Program(vertex_shader, fragment_shader)
+
+# We generate the x, y coordinates of the points.
+position = .25 * np.random.randn(100000, 2)
+
+# We set the a_position attribute to the position NumPy array.
+# Every row in the array is one vertex. The vertex shader is called once per
+# vertex, so once per row.
+# WARNING: most GPUs only support float32 data (not float64).
+program['a_position'] = position.astype(np.float32)
+
+# We create a ccanvas.
+canvas = app.Canvas(keys='interactive')
+
+# We update the viewport when the window is resized.
+ at canvas.connect
+def on_resize(event):
+    width, height = event.size
+    gloo.set_viewport(0, 0, width, height)
+
+ at canvas.connect
+def on_draw(event):
+    # We clear the window.
+    gloo.clear('white')
+
+    # We render the program with the 'points' mode. Every vertex is
+    # rendered as a point.
+    program.draw('points')
+
+canvas.show()
+app.run()
+```
+
+
+### GPU-based animations in GLSL
+
+For optimal performance, interactivity and animations should be implemented on the GPU. Vertex positions can be updated in real-time on the GPU, which is used for panning and zooming for example. This is optimal because vertices are all processed in parallel on the GPU. Also, the fragment shader can be used for ray tracing, fractals, and volume rendering.
+
+Here is a simple example where an animation is implemented on the GPU:
+
+```python
+from vispy import app, gloo
+import numpy as np
+
+# We manually tesselate an horizontal rectangle. We will use triangle strips.
+n = 100
+position = np.zeros((2*n, 2)).astype(np.float32)
+position[:,0] = np.repeat(np.linspace(-1, 1, n), 2)
+position[::2,1] = -.2
+position[1::2,1] = .2
+color = np.linspace(0., 1., 2 * n).astype(np.float32)
+
+vertex_shader = """
+const float M_PI = 3.14159265358979323846;
+
+attribute vec2 a_position;
+attribute float a_color;
+
+// Varyings are used to passed values from the vertex shader to the fragment
+// shader. The value at one pixel in the fragment shader is interpolated
+// from the values at the neighboring pixels.
+varying float v_color;
+uniform float u_time;
+
+void main (void) {
+    // We implement the animation in the y coordinate.
+    float x = a_position.x;
+    float y = a_position.y + .1 * cos(2.0*M_PI*(u_time-.5*x));
+    gl_Position = vec4(x, y, 0.0, 1.0);
+    
+    // We pass the color to the fragment shader.
+    v_color = a_color;
+}
+"""
+
+fragment_shader = """
+uniform float u_time;
+varying float v_color;
+void main()
+{
+    gl_FragColor = vec4(1.0, v_color, 0.0, 1.0);
+}
+"""
+
+# This is an alternative way to create a canvas: deriving from app.Canvas.
+class Canvas(app.Canvas):
+    def __init__(self):
+        app.Canvas.__init__(self, keys='interactive')
+        self.program = gloo.Program(vertex_shader, fragment_shader)
+        self.program['a_position'] = gloo.VertexBuffer(position)
+        self.program['a_color'] = gloo.VertexBuffer(color)
+        self.program['u_time'] = 0.
+
+        # We create a timer for the animation.
+        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
+
+    def on_resize(self, event):
+        width, height = event.size
+        gloo.set_viewport(0, 0, width, height)
+
+    def on_draw(self, event):
+        gloo.clear(color=(0.0, 0.0, 0.0, 1.0))
+        self.program.draw('triangle_strip')
+
+    def on_timer(self, event):
+        # We update theu_time uniform at every iteration.
+        self.program['u_time'] = event.iteration * 1. / 60
+
+        # This is used to refresh the window and trigger a draw event.
+        self.update()
+
+canvas = Canvas()
+canvas.show()
+app.run()
+```
diff --git a/examples/basics/gloo/animate_images.py b/examples/basics/gloo/animate_images.py
index 9cb08cf..a0f0bc2 100644
--- a/examples/basics/gloo/animate_images.py
+++ b/examples/basics/gloo/animate_images.py
@@ -60,12 +60,10 @@ void main()
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-        self.size = W * 5, H * 5
+        app.Canvas.__init__(self, keys='interactive', size=((W * 5), (H * 5)))
 
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
-        self.texture = gloo.Texture2D(I)
-        self.texture.interpolation = 'linear'
+        self.texture = gloo.Texture2D(I, interpolation='linear')
 
         self.program['u_texture'] = self.texture
         self.program.bind(gloo.VertexBuffer(data))
@@ -78,14 +76,15 @@ class Canvas(app.Canvas):
         self.program['u_view'] = self.view
         self.projection = ortho(0, W, 0, H, -1, 1)
         self.program['u_projection'] = self.projection
-        
-        self._timer = app.Timer('auto', connect=self.update, start=True)
 
-    def on_initialize(self, event):
         gloo.set_clear_color('white')
 
+        self._timer = app.Timer('auto', connect=self.update, start=True)
+
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
+        width, height = event.physical_size
         gloo.set_viewport(0, 0, width, height)
         self.projection = ortho(0, width, 0, height, -100, 100)
         self.program['u_projection'] = self.projection
@@ -112,5 +111,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/basics/gloo/animate_images_slice.py b/examples/basics/gloo/animate_images_slice.py
index b183f40..edbded6 100644
--- a/examples/basics/gloo/animate_images_slice.py
+++ b/examples/basics/gloo/animate_images_slice.py
@@ -14,6 +14,7 @@ import numpy as np
 from vispy.util.transforms import ortho
 from vispy import gloo
 from vispy import app
+from vispy.visuals.shaders import ModularProgram
 
 
 # Shape of image to be displayed
@@ -57,13 +58,13 @@ void main (void)
 """
 
 FRAG_SHADER = """
-uniform sampler3D u_texture;
+uniform $sampler_type u_texture;
 uniform float i;
 varying vec2 v_texcoord;
 void main()
 {
     // step through gradient with i, note that slice (depth) comes last here!
-    gl_FragColor = texture3D(u_texture, vec3(v_texcoord, i));
+    gl_FragColor = $sample(u_texture, vec3(v_texcoord, i));
     gl_FragColor.a = 1.0;
 }
 
@@ -72,15 +73,19 @@ void main()
 
 class Canvas(app.Canvas):
 
-    def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-        self.size = W * 5, H * 5
+    def __init__(self, emulate3d=True):
+        app.Canvas.__init__(self, keys='interactive', size=((W*5), (H*5)))
 
-        self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
-        self.texture = gloo.Texture3D(I)
-        self.texture.interpolation = 'nearest'
-        self.texture.wrapping = 'clamp_to_edge'
+        if emulate3d:
+            tex_cls = gloo.TextureEmulated3D
+        else:
+            tex_cls = gloo.Texture3D
+        self.texture = tex_cls(I, interpolation='nearest',
+                               wrapping='clamp_to_edge')
 
+        self.program = ModularProgram(VERT_SHADER, FRAG_SHADER)
+        self.program.frag['sampler_type'] = self.texture.glsl_sampler_type
+        self.program.frag['sample'] = self.texture.glsl_sample
         self.program['u_texture'] = self.texture
         self.program['i'] = 0.0
         self.program.bind(gloo.VertexBuffer(data))
@@ -96,13 +101,14 @@ class Canvas(app.Canvas):
 
         self.i = 0
 
+        gloo.set_clear_color('white')
+
         self._timer = app.Timer('auto', connect=self.on_timer, start=True)
 
-    def on_initialize(self, event):
-        gloo.set_clear_color('white')
+        self.show()
 
     def on_resize(self, event):
-        width, height = event.size
+        width, height = event.physical_size
         gloo.set_viewport(0, 0, width, height)
         self.projection = ortho(0, width, 0, height, -100, 100)
         self.program['u_projection'] = self.projection
@@ -132,6 +138,6 @@ class Canvas(app.Canvas):
 
 
 if __name__ == '__main__':
-    c = Canvas()
-    c.show()
+    # Use emulated3d to switch from an emulated 3D texture to an actual one
+    c = Canvas(emulate3d=True)
     app.run()
diff --git a/examples/basics/gloo/animate_shape.py b/examples/basics/gloo/animate_shape.py
index aac9b06..43e2aed 100644
--- a/examples/basics/gloo/animate_shape.py
+++ b/examples/basics/gloo/animate_shape.py
@@ -79,14 +79,15 @@ class Canvas(app.Canvas):
         self._program.bind(self._vbo)  # This does:
         #self._program['a_position'] = self._vbo['a_position']
         #self._program['a_texcoords'] = self._vbo['a_texcoords']
-        
-        self._timer = app.Timer('auto', connect=self.update, start=True)
-    
-    def on_initialize(self, event):
+
         gloo.set_clear_color('white')
 
+        self._timer = app.Timer('auto', connect=self.update, start=True)
+
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
+        width, height = event.physical_size
         gloo.set_viewport(0, 0, width, height)
 
     def on_draw(self, event):
@@ -106,5 +107,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/basics/gloo/display_lines.py b/examples/basics/gloo/display_lines.py
index 2983495..6b4e32a 100644
--- a/examples/basics/gloo/display_lines.py
+++ b/examples/basics/gloo/display_lines.py
@@ -4,6 +4,11 @@
 """ Show a bunch of lines.
 This example demonstrates how multiple line-pieces can be drawn
 using one call, by discarting some fragments.
+
+Note that this example uses canvas.context.X() to call gloo functions.
+These functions are also available as vispy.gloo.X(), but apply
+explicitly to the canvas. We still need to decide which we think is the
+preferred API.
 """
 
 import numpy as np
@@ -11,9 +16,9 @@ from vispy import gloo
 from vispy import app
 from vispy.util.transforms import perspective, translate, rotate
 
-# app.use_app('glut')
+W, H = 400, 400
 
-# Create vetices
+# Create vertices
 n = 100
 a_position = np.random.uniform(-1, 1, (n, 3)).astype(np.float32)
 a_id = np.random.randint(0, 30, (n, 1))
@@ -51,7 +56,7 @@ class Canvas(app.Canvas):
 
     # ---------------------------------
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
+        app.Canvas.__init__(self, keys='interactive', size=(W, H))
 
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
 
@@ -59,24 +64,27 @@ class Canvas(app.Canvas):
         self.program['a_id'] = gloo.VertexBuffer(a_id)
         self.program['a_position'] = gloo.VertexBuffer(a_position)
 
-        self.view = np.eye(4, dtype=np.float32)
+        self.translate = 5
+        self.view = translate((0, 0, -self.translate), dtype=np.float32)
         self.model = np.eye(4, dtype=np.float32)
-        self.projection = np.eye(4, dtype=np.float32)
 
-        self.translate = 5
-        translate(self.view, 0, 0, -self.translate)
+        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])
+        self.projection = perspective(45.0, self.size[0] /
+                                      float(self.size[1]), 1.0, 1000.0)
+        self.program['u_projection'] = self.projection
+
         self.program['u_model'] = self.model
         self.program['u_view'] = self.view
 
         self.theta = 0
         self.phi = 0
 
+        self.context.set_clear_color('white')
+        self.context.set_state('translucent')
+
         self.timer = app.Timer('auto', connect=self.on_timer)
 
-    # ---------------------------------
-    def on_initialize(self, event):
-        gloo.set_clear_color('white')
-        gloo.set_state('translucent')
+        self.show()
 
     # ---------------------------------
     def on_key_press(self, event):
@@ -90,35 +98,32 @@ class Canvas(app.Canvas):
     def on_timer(self, event):
         self.theta += .5
         self.phi += .5
-        self.model = np.eye(4, dtype=np.float32)
-        rotate(self.model, self.theta, 0, 0, 1)
-        rotate(self.model, self.phi, 0, 1, 0)
+        self.model = np.dot(rotate(self.theta, (0, 0, 1)),
+                            rotate(self.phi, (0, 1, 0)))
         self.program['u_model'] = self.model
         self.update()
 
     # ---------------------------------
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = perspective(45.0, width / float(height), 1.0, 1000.0)
+        gloo.set_viewport(0, 0, event.physical_size[0], event.physical_size[1])
+        self.projection = perspective(45.0, event.size[0] /
+                                      float(event.size[1]), 1.0, 1000.0)
         self.program['u_projection'] = self.projection
 
     # ---------------------------------
     def on_mouse_wheel(self, event):
         self.translate += event.delta[1]
         self.translate = max(2, self.translate)
-        self.view = np.eye(4, dtype=np.float32)
-        translate(self.view, 0, 0, -self.translate)
+        self.view = translate((0, 0, -self.translate))
         self.program['u_view'] = self.view
         self.update()
 
     # ---------------------------------
     def on_draw(self, event):
-        gloo.clear()
+        self.context.clear()
         self.program.draw('line_strip')
 
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/basics/gloo/display_points.py b/examples/basics/gloo/display_points.py
index c9db79f..5449de4 100644
--- a/examples/basics/gloo/display_points.py
+++ b/examples/basics/gloo/display_points.py
@@ -8,14 +8,8 @@ from vispy import gloo
 from vispy import app
 import numpy as np
 
-# Create vetices
-n = 10000
-v_position = 0.25 * np.random.randn(n, 2).astype(np.float32)
-v_color = np.random.uniform(0, 1, (n, 3)).astype(np.float32)
-v_size = np.random.uniform(2, 12, (n, 1)).astype(np.float32)
-
 VERT_SHADER = """
-attribute vec3  a_position;
+attribute vec2  a_position;
 attribute vec3  a_color;
 attribute float a_size;
 
@@ -32,7 +26,7 @@ void main (void) {
     v_fg_color  = vec4(0.0,0.0,0.0,0.5);
     v_bg_color  = vec4(a_color,    1.0);
 
-    gl_Position = vec4(a_position, 1.0);
+    gl_Position = vec4(a_position, 0.0, 1.0);
     gl_PointSize = 2.0*(v_radius + v_linewidth + 1.5*v_antialias);
 }
 """
@@ -70,8 +64,14 @@ class Canvas(app.Canvas):
 
     def __init__(self):
         app.Canvas.__init__(self, keys='interactive')
+        ps = self.pixel_scale
+
+        # Create vertices
+        n = 10000
+        v_position = 0.25 * np.random.randn(n, 2).astype(np.float32)
+        v_color = np.random.uniform(0, 1, (n, 3)).astype(np.float32)
+        v_size = np.random.uniform(2*ps, 12*ps, (n, 1)).astype(np.float32)
 
-    def on_initialize(self, event):
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
         # Set uniform and attribute
         self.program['a_color'] = gloo.VertexBuffer(v_color)
@@ -79,9 +79,11 @@ class Canvas(app.Canvas):
         self.program['a_size'] = gloo.VertexBuffer(v_size)
         gloo.set_state(clear_color='white', blend=True,
                        blend_func=('src_alpha', 'one_minus_src_alpha'))
-    
+
+        self.show()
+
     def on_resize(self, event):
-        gloo.set_viewport(0, 0, *event.size)
+        gloo.set_viewport(0, 0, *event.physical_size)
 
     def on_draw(self, event):
         gloo.clear(color=True, depth=True)
@@ -90,5 +92,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/basics/gloo/display_shape.py b/examples/basics/gloo/display_shape.py
index ce7e385..7eb8faf 100644
--- a/examples/basics/gloo/display_shape.py
+++ b/examples/basics/gloo/display_shape.py
@@ -44,11 +44,12 @@ class Canvas(app.Canvas):
         self._program['u_color'] = 0.2, 1.0, 0.4, 1
         self._program['a_position'] = gloo.VertexBuffer(vPosition)
 
-    def on_initialize(self, event):
         gloo.set_clear_color('white')
 
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
+        width, height = event.physical_size
         gloo.set_viewport(0, 0, width, height)
 
     def on_draw(self, event):
@@ -58,5 +59,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/basics/gloo/gpuimage.py b/examples/basics/gloo/gpuimage.py
index 9a3515f..025f3cc 100644
--- a/examples/basics/gloo/gpuimage.py
+++ b/examples/basics/gloo/gpuimage.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # vispy: gallery 100
@@ -21,7 +21,8 @@ void main()
 """
 
 fragment = """
-const float M_PI = 3.14159265358979323846;
+#include "math/constants.glsl"
+//const float M_PI = 3.14159265358979323846;
 uniform float u_time;
 varying vec2 v_position;
 
@@ -92,14 +93,17 @@ class Canvas(app.Canvas):
         self.program['a_position'] = [(-1., -1.), (-1., +1.),
                                       (+1., -1.), (+1., +1.)]
 
+        self.program['u_time'] = 0.0
         self.timer = app.Timer('auto', connect=self.on_timer, start=True)
 
+        self.show()
+
     def on_timer(self, event):
         self.program['u_time'] = event.elapsed
         self.update()
 
     def on_resize(self, event):
-        width, height = event.size
+        width, height = event.physical_size
         gloo.set_viewport(0, 0, width, height)
 
     def on_draw(self, event):
@@ -107,5 +111,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     canvas = Canvas()
-    canvas.show()
     app.run()
diff --git a/examples/basics/gloo/hello_fbo.py b/examples/basics/gloo/hello_fbo.py
index ccfc2e5..db6f268 100644
--- a/examples/basics/gloo/hello_fbo.py
+++ b/examples/basics/gloo/hello_fbo.py
@@ -75,33 +75,30 @@ SIZE = 50
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-        self.size = 560, 420
+        app.Canvas.__init__(self, keys='interactive', size=(560, 420))
 
         # Create texture to render to
-        shape = self.size[1], self.size[0]
-        self._rendertex = gloo.Texture2D(shape=(shape + (3,)),
-                                         dtype=np.float32)
+        shape = self.physical_size[1], self.physical_size[0]
+        self._rendertex = gloo.Texture2D((shape + (3,)))
 
         # Create FBO, attach the color buffer and depth buffer
-        self._fbo = gloo.FrameBuffer(self._rendertex,
-                                     gloo.DepthBuffer(shape))
+        self._fbo = gloo.FrameBuffer(self._rendertex, gloo.RenderBuffer(shape))
 
         # Create program to render a shape
-        self._program1 = gloo.Program(gloo.VertexShader(VERT_SHADER1),
-                                      gloo.FragmentShader(FRAG_SHADER1))
+        self._program1 = gloo.Program(VERT_SHADER1, FRAG_SHADER1)
         self._program1['u_color'] = 0.9, 1.0, 0.4, 1
         self._program1['a_position'] = gloo.VertexBuffer(vPosition)
 
         # Create program to render FBO result
-        self._program2 = gloo.Program(gloo.VertexShader(VERT_SHADER2),
-                                      gloo.FragmentShader(FRAG_SHADER2))
+        self._program2 = gloo.Program(VERT_SHADER2, FRAG_SHADER2)
         self._program2['a_position'] = gloo.VertexBuffer(vPosition)
         self._program2['a_texcoord'] = gloo.VertexBuffer(vTexcoord)
         self._program2['u_texture1'] = self._rendertex
 
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
+        width, height = event.physical_size
         gloo.set_viewport(0, 0, width, height)
 
     def on_draw(self, event):
@@ -109,7 +106,7 @@ class Canvas(app.Canvas):
         with self._fbo:
             gloo.set_clear_color((0.0, 0.0, 0.5, 1))
             gloo.clear(color=True, depth=True)
-            gloo.set_viewport(0, 0, *self.size)
+            gloo.set_viewport(0, 0, *self.physical_size)
             self._program1.draw('triangle_strip')
 
         # Now draw result to a full-screen quad
@@ -121,5 +118,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/basics/gloo/multi_texture.py b/examples/basics/gloo/multi_texture.py
new file mode 100644
index 0000000..3a9f24f
--- /dev/null
+++ b/examples/basics/gloo/multi_texture.py
@@ -0,0 +1,88 @@
+# !/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""Example demonstrating (and testing) multi-texturing.
+
+We create two textures. One that shows a red, green and blue band in
+the horizontal direction and one that does the same in the vertical
+direction. In the fragment shader the colors from both textures are
+added.
+"""
+
+import numpy as np
+
+from vispy import gloo
+from vispy import app
+
+
+# Images to be displayed
+W, H = 30, 30
+im1 = np.zeros((W, H, 3), np.float32)
+im2 = np.zeros((W, H, 3), np.float32)
+im1[:10, :, 0] = 1.0
+im1[10:20, :, 1] = 1.0
+im1[20:, :, 2] = 1.0
+im2[:, :10, 0] = 1.0
+im2[:, 10:20, 1] = 1.0
+im1[:, 20:, 2] = 1.0
+
+# A simple texture quad
+data = np.zeros(4, dtype=[('a_position', np.float32, 2),
+                          ('a_texcoord', np.float32, 2)])
+data['a_position'] = np.array([[-1, -1], [+1, -1], [-1, +1], [+1, +1]])
+data['a_texcoord'] = np.array([[1, 0], [1, 1.2], [0, 0], [0, 1.2]])
+
+
+VERT_SHADER = """
+attribute vec2 a_position;
+attribute vec2 a_texcoord;
+varying vec2 v_texcoord;
+
+void main (void)
+{
+    v_texcoord = a_texcoord;
+    gl_Position = vec4(a_position, 0.0, 1.0);
+}
+"""
+
+FRAG_SHADER = """
+uniform sampler2D u_tex1;
+uniform sampler2D u_tex2;
+varying vec2 v_texcoord;
+
+void main()
+{
+    vec3 clr1 = texture2D(u_tex1, v_texcoord).rgb;
+    vec3 clr2 = texture2D(u_tex2, v_texcoord).rgb;
+    gl_FragColor.rgb = clr1 + clr2;
+    gl_FragColor.a = 1.0;
+}
+"""
+
+
+class Canvas(app.Canvas):
+
+    def __init__(self):
+        app.Canvas.__init__(self, size=(500, 500), keys='interactive')
+
+        self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
+        self.program['u_tex1'] = gloo.Texture2D(im1, interpolation='linear')
+        self.program['u_tex2'] = gloo.Texture2D(im2, interpolation='linear')
+        self.program.bind(gloo.VertexBuffer(data))
+
+        gloo.set_clear_color('white')
+
+        self.show()
+
+    def on_resize(self, event):
+        width, height = event.physical_size
+        gloo.set_viewport(0, 0, width, height)
+
+    def on_draw(self, event):
+        gloo.clear(color=True, depth=True)
+        self.program.draw('triangle_strip')
+
+
+if __name__ == '__main__':
+    c = Canvas()
+    app.run()
diff --git a/examples/basics/gloo/post_processing.py b/examples/basics/gloo/post_processing.py
index 34edabb..1e59055 100644
--- a/examples/basics/gloo/post_processing.py
+++ b/examples/basics/gloo/post_processing.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author:   Nicolas P .Rougier
@@ -16,7 +16,8 @@ from vispy import app
 from vispy.geometry import create_cube
 from vispy.util.transforms import perspective, translate, rotate
 from vispy.gloo import (Program, VertexBuffer, IndexBuffer, Texture2D, clear,
-                        FrameBuffer, DepthBuffer, set_viewport, set_state)
+                        FrameBuffer, RenderBuffer, set_viewport, set_state)
+
 
 cube_vertex = """
 uniform mat4 model;
@@ -24,8 +25,8 @@ uniform mat4 view;
 uniform mat4 projection;
 attribute vec3 position;
 attribute vec2 texcoord;
-attribute vec3 normal;
-attribute vec4 color;
+attribute vec3 normal;  // not used in this example
+attribute vec4 color;  // not used in this example
 varying vec2 v_texcoord;
 void main()
 {
@@ -85,7 +86,6 @@ class Canvas(app.Canvas):
         app.Canvas.__init__(self, title='Framebuffer post-processing',
                             keys='interactive', size=(512, 512))
 
-    def on_initialize(self, event):
         # Build cube data
         # --------------------------------------
         vertices, indices, _ = create_cube()
@@ -94,12 +94,9 @@ class Canvas(app.Canvas):
 
         # Build program
         # --------------------------------------
-        view = np.eye(4, dtype=np.float32)
-        model = np.eye(4, dtype=np.float32)
-        translate(view, 0, 0, -7)
+        view = translate((0, 0, -7))
         self.phi, self.theta = 60, 20
-        rotate(model, self.theta, 0, 0, 1)
-        rotate(model, self.phi, 0, 1, 0)
+        model = rotate(self.theta, (0, 0, 1)).dot(rotate(self.phi, (0, 1, 0)))
 
         self.cube = Program(cube_vertex, cube_fragment)
         self.cube.bind(vertices)
@@ -108,35 +105,35 @@ class Canvas(app.Canvas):
         self.cube['model'] = model
         self.cube['view'] = view
 
-        depth = DepthBuffer((512, 512))
-        color = Texture2D(shape=(512, 512, 3), dtype=np.dtype(np.float32))
-        self.framebuffer = FrameBuffer(color=color, depth=depth)
+        color = Texture2D((512, 512, 3), interpolation='linear')
+        self.framebuffer = FrameBuffer(color, RenderBuffer((512, 512)))
 
         self.quad = Program(quad_vertex, quad_fragment, count=4)
         self.quad['texcoord'] = [(0, 0), (0, 1), (1, 0), (1, 1)]
         self.quad['position'] = [(-1, -1), (-1, +1), (+1, -1), (+1, +1)]
         self.quad['texture'] = color
-        self.quad["texture"].interpolation = 'linear'
 
         # OpenGL and Timer initalization
         # --------------------------------------
         set_state(clear_color=(.3, .3, .35, 1), depth_test=True)
         self.timer = app.Timer('auto', connect=self.on_timer, start=True)
-        self._set_projection(self.size)
+        self._set_projection(self.physical_size)
+
+        self.show()
 
     def on_draw(self, event):
-        self.framebuffer.activate()
-        set_viewport(0, 0, 512, 512)
-        clear(color=True, depth=True)
-        set_state(depth_test=True)
-        self.cube.draw('triangles', self.indices)
-        self.framebuffer.deactivate()
+        with self.framebuffer:
+            set_viewport(0, 0, 512, 512)
+            clear(color=True, depth=True)
+            set_state(depth_test=True)
+            self.cube.draw('triangles', self.indices)
+        set_viewport(0, 0, *self.physical_size)
         clear(color=True)
         set_state(depth_test=False)
         self.quad.draw('triangle_strip')
 
     def on_resize(self, event):
-        self._set_projection(event.size)
+        self._set_projection(event.physical_size)
 
     def _set_projection(self, size):
         width, height = size
@@ -147,13 +144,10 @@ class Canvas(app.Canvas):
     def on_timer(self, event):
         self.theta += .5
         self.phi += .5
-        model = np.eye(4, dtype=np.float32)
-        rotate(model, self.theta, 0, 0, 1)
-        rotate(model, self.phi, 0, 1, 0)
+        model = rotate(self.theta, (0, 0, 1)).dot(rotate(self.phi, (0, 1, 0)))
         self.cube['model'] = model
         self.update()
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     c.app.run()
diff --git a/examples/basics/gloo/rotate_cube.py b/examples/basics/gloo/rotate_cube.py
index b20dbdb..e9c54f4 100644
--- a/examples/basics/gloo/rotate_cube.py
+++ b/examples/basics/gloo/rotate_cube.py
@@ -10,7 +10,6 @@ import numpy as np
 from vispy import app, gloo
 from vispy.util.transforms import perspective, translate, rotate
 
-
 vert = """
 // Uniforms
 // ------------------------------------
@@ -98,8 +97,7 @@ def cube():
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-        self.size = 800, 600
+        app.Canvas.__init__(self, keys='interactive', size=(800, 600))
 
         self.vertices, self.filled, self.outline = cube()
         self.filled_buf = gloo.IndexBuffer(self.filled)
@@ -108,41 +106,43 @@ class Canvas(app.Canvas):
         self.program = gloo.Program(vert, frag)
         self.program.bind(gloo.VertexBuffer(self.vertices))
 
-        self.view = np.eye(4, dtype=np.float32)
+        self.view = translate((0, 0, -5))
         self.model = np.eye(4, dtype=np.float32)
-        self.projection = np.eye(4, dtype=np.float32)
 
-        translate(self.view, 0, 0, -5)
+        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])
+        self.projection = perspective(45.0, self.size[0] /
+                                      float(self.size[1]), 2.0, 10.0)
+
+        self.program['u_projection'] = self.projection
+
         self.program['u_model'] = self.model
         self.program['u_view'] = self.view
 
         self.theta = 0
         self.phi = 0
 
-        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
-    
-    # ---------------------------------
-    def on_initialize(self, event):
         gloo.set_clear_color('white')
         gloo.set_state('opaque')
         gloo.set_polygon_offset(1, 1)
-        # gl.glEnable( gl.GL_LINE_SMOOTH )
+
+        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
+
+        self.show()
 
     # ---------------------------------
     def on_timer(self, event):
         self.theta += .5
         self.phi += .5
-        self.model = np.eye(4, dtype=np.float32)
-        rotate(self.model, self.theta, 0, 0, 1)
-        rotate(self.model, self.phi, 0, 1, 0)
+        self.model = np.dot(rotate(self.theta, (0, 1, 0)),
+                            rotate(self.phi, (0, 0, 1)))
         self.program['u_model'] = self.model
         self.update()
 
     # ---------------------------------
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = perspective(45.0, width / float(height), 2.0, 10.0)
+        gloo.set_viewport(0, 0, event.physical_size[0], event.physical_size[1])
+        self.projection = perspective(45.0, event.size[0] /
+                                      float(event.size[1]), 2.0, 10.0)
         self.program['u_projection'] = self.projection
 
     # ---------------------------------
@@ -150,7 +150,7 @@ class Canvas(app.Canvas):
         gloo.clear()
 
         # Filled cube
-        
+
         gloo.set_state(blend=False, depth_test=True, polygon_offset_fill=True)
         self.program['u_color'] = 1, 1, 1, 1
         self.program.draw('triangles', self.filled_buf)
@@ -166,5 +166,4 @@ class Canvas(app.Canvas):
 # -----------------------------------------------------------------------------
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/basics/gloo/start.py b/examples/basics/gloo/start.py
index bc850b7..566bae1 100644
--- a/examples/basics/gloo/start.py
+++ b/examples/basics/gloo/start.py
@@ -2,17 +2,19 @@
 # -*- coding: utf-8 -*-
 """ Probably the simplest vispy example
 """
+import sys
 
-from vispy import app
-from vispy import gloo
+from vispy import app, gloo
 
-c = app.Canvas(show=True, keys='interactive')
+canvas = app.Canvas(keys='interactive')
 
 
- at c.connect
+ at canvas.connect
 def on_draw(event):
     gloo.set_clear_color((0.2, 0.4, 0.6, 1.0))
     gloo.clear()
 
-if __name__ == '__main__':
+canvas.show()
+
+if __name__ == '__main__' and sys.flags.interactive == 0:
     app.run()
diff --git a/examples/basics/gloo/start_shaders.py b/examples/basics/gloo/start_shaders.py
index 3e9b09a..7b6b5cb 100644
--- a/examples/basics/gloo/start_shaders.py
+++ b/examples/basics/gloo/start_shaders.py
@@ -1,18 +1,22 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import sys
+
 from vispy import gloo
 from vispy import app
 import numpy as np
 
 VERT_SHADER = """
-#version 120
 attribute vec2 a_position;
+uniform float u_size;
+
 void main() {
     gl_Position = vec4(a_position, 0.0, 1.0);
-    gl_PointSize = 10.0;
+    gl_PointSize = u_size;
 }
 """
 
 FRAG_SHADER = """
-#version 120
 void main() {
     gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
 }
@@ -22,8 +26,15 @@ void main() {
 class Canvas(app.Canvas):
     def __init__(self):
         app.Canvas.__init__(self, keys='interactive')
+
+        ps = self.pixel_scale
+
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
-        self.program['a_position'] = np.random.uniform(-0.5, 0.5, size=(2, 20))
+        data = np.random.uniform(-0.5, 0.5, size=(20, 2))
+        self.program['a_position'] = data.astype(np.float32)
+        self.program['u_size'] = 20.*ps
+
+        self.show()
 
     def on_resize(self, event):
         width, height = event.size
@@ -33,6 +44,7 @@ class Canvas(app.Canvas):
         gloo.clear('white')
         self.program.draw('points')
 
-c = Canvas()
-c.show()
-app.run()
+if __name__ == '__main__':
+    c = Canvas()
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/examples/basics/plotting/export.py b/examples/basics/plotting/export.py
new file mode 100644
index 0000000..d3a597b
--- /dev/null
+++ b/examples/basics/plotting/export.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+"""
+Demonstrates rendering a canvas to an image at higher resolution than the
+original display.
+"""
+
+import vispy.plot as vp
+
+# Create a canvas showing plot data
+fig = vp.Fig()
+fig[0, 0].plot([1, 6, 2, 4, 3, 8, 5, 7, 6, 3])
+
+# Render the canvas scene to a numpy array image with higher resolution
+# than the original canvas
+scale = 4
+image = fig.render(size=(fig.size[0]*scale, fig.size[1]*scale))
+
+# Display the data in the array, sub-sampled down to the original canvas
+# resolution
+fig_2 = vp.Fig()
+fig_2[0, 0].image(image[::-scale, ::scale])
+
+# By default, the view adds some padding when setting its range.
+# We'll remove that padding so the image looks exactly like the original
+# canvas:
+fig_2[0, 0].camera.set_range(margin=0)
+
+if __name__ == '__main__':
+    fig.app.run()
diff --git a/examples/basics/plotting/mpl_plot.py b/examples/basics/plotting/mpl_plot.py
index a608686..f7d589f 100644
--- a/examples/basics/plotting/mpl_plot.py
+++ b/examples/basics/plotting/mpl_plot.py
@@ -1,6 +1,9 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
 """
 Example demonstrating how to use vispy.pyplot, which uses mplexporter
 to convert matplotlib commands to vispy draw commands.
@@ -9,9 +12,11 @@ Requires matplotlib.
 """
 
 import numpy as np
-import sys
 
+# You can use either matplotlib or vispy to render this example:
+# import matplotlib.pyplot as plt
 import vispy.mpl_plot as plt
+
 from vispy.io import read_png, load_data_file
 
 n = 200
@@ -47,5 +52,5 @@ plt.draw()
 # 2. Any plotting commands executed after this will not take effect.
 # We are working to remove this limitation.
 
-block = False if sys.flags.interactive else True
-plt.show(block)
+if __name__ == '__main__':
+    plt.show(True)
diff --git a/examples/basics/plotting/scatter_histogram.py b/examples/basics/plotting/scatter_histogram.py
new file mode 100644
index 0000000..0f13e7b
--- /dev/null
+++ b/examples/basics/plotting/scatter_histogram.py
@@ -0,0 +1,25 @@
+# !/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: gallery 30
+
+"""
+A scatter plot of 2D points with matching histograms.
+"""
+
+import numpy as np
+
+import vispy.plot as vp
+
+n = 100000
+data = np.random.randn(n, 2)
+color = (0.8, 0.25, 0.)
+n_bins = 100
+
+fig = vp.Fig(show=False)
+fig[0:4, 0:4].plot(data, width=0, face_color=color + (0.05,), edge_color=None,
+                   marker_size=10.)
+fig[4, 0:4].histogram(data[:, 0], bins=n_bins, color=color, orientation='h')
+fig[0:4, 4].histogram(data[:, 1], bins=n_bins, color=color, orientation='v')
+
+if __name__ == '__main__':
+    fig.show(run=True)
diff --git a/examples/basics/plotting/spectrogram.py b/examples/basics/plotting/spectrogram.py
new file mode 100644
index 0000000..f158634
--- /dev/null
+++ b/examples/basics/plotting/spectrogram.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# vispy: gallery 1
+"""
+A spectrogram and waveform plot of 1D data.
+"""
+
+import numpy as np
+
+from vispy import plot as vp
+
+# Create a logarithmic chirp
+fs = 1000.
+N = 10000
+t = np.arange(N) / float(fs)
+f0, f1 = 1., 500.
+phase = (t[-1] / np.log(f1 / f0)) * f0 * (pow(f1 / f0, t / t[-1]) - 1.0)
+data = np.cos(2 * np.pi * phase)
+
+fig = vp.Fig(size=(800, 400), show=False)
+fig[0:2, 0].spectrogram(data, fs=fs, clim=(-100, -20))
+fig[2, 0].plot(np.array((t, data)).T, marker_size=0)
+
+if __name__ == '__main__':
+    fig.show(run=True)
diff --git a/examples/basics/plotting/vispy_plot.py b/examples/basics/plotting/vispy_plot.py
deleted file mode 100644
index 2c2e024..0000000
--- a/examples/basics/plotting/vispy_plot.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-import numpy as np
-import vispy.plot as vplt
-
-plot_data = [1, 6, 2, 4, 3, 8, 4, 6, 5, 2]
-canvas1 = vplt.plot(plot_data)
-
-image_data = np.random.normal(size=(20, 20), loc=128, scale=60)
-canvas2 = vplt.image(image_data.astype(np.ubyte))
-
-
-# Start up the event loop if this is not an interactive prompt.
-import sys
-if sys.flags.interactive == 0:
-    canvas1.app.run()
diff --git a/examples/basics/plotting/volume.py b/examples/basics/plotting/volume.py
new file mode 100644
index 0000000..2544cab
--- /dev/null
+++ b/examples/basics/plotting/volume.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# vispy: gallery 1
+"""
+Plot various views of a structural MRI.
+"""
+
+import numpy as np
+
+from vispy import io, plot as vp
+
+fig = vp.Fig(bgcolor='k', size=(800, 800), show=False)
+
+vol_data = np.load(io.load_data_file('brain/mri.npz'))['data']
+vol_data = np.flipud(np.rollaxis(vol_data, 1))
+
+clim = [32, 192]
+vol_pw = fig[0, 0]
+vol_pw.volume(vol_data, clim=clim)
+vol_pw.camera.elevation = 30
+vol_pw.camera.azimuth = 30
+vol_pw.camera.scale_factor /= 1.5
+
+shape = vol_data.shape
+fig[1, 0].image(vol_data[:, :, shape[2] // 2], cmap='grays', clim=clim)
+fig[0, 1].image(vol_data[:, shape[1] // 2, :], cmap='grays', clim=clim)
+fig[1, 1].image(vol_data[shape[0] // 2, :, :].T, cmap='grays', clim=clim)
+
+if __name__ == '__main__':
+    fig.show(run=True)
diff --git a/examples/basics/scene/background_borders.py b/examples/basics/scene/background_borders.py
new file mode 100644
index 0000000..85c1c36
--- /dev/null
+++ b/examples/basics/scene/background_borders.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# vispy: gallery 30
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Demonstration of borders and background colors.
+"""
+
+from vispy.scene import SceneCanvas
+
+canvas = SceneCanvas(keys='interactive', bgcolor='w', show=True)
+grid = canvas.central_widget.add_grid(spacing=0, bgcolor='gray',
+                                      border_color='k')
+view1 = grid.add_view(row=0, col=0, margin=10, bgcolor=(1, 0, 0, 0.5),
+                      border_color=(1, 0, 0))
+view2 = grid.add_view(row=0, col=1, margin=10, bgcolor=(0, 1, 0, 0.5),
+                      border_color=(0, 1, 0))
+
+if __name__ == '__main__':
+    canvas.app.run()
diff --git a/examples/basics/scene/colored_line.py b/examples/basics/scene/colored_line.py
new file mode 100644
index 0000000..4bcda25
--- /dev/null
+++ b/examples/basics/scene/colored_line.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+# vispy: gallery 30
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Demonstration of various features of Line visual.
+"""
+import itertools
+import numpy as np
+
+from vispy import app, scene
+from vispy.color import get_colormaps
+from vispy.visuals.transforms import STTransform
+from vispy.ext.six import next
+
+colormaps = itertools.cycle(get_colormaps())
+
+# vertex positions of data to draw
+N = 200
+pos = np.zeros((N, 2), dtype=np.float32)
+pos[:, 0] = np.linspace(10, 390, N)
+pos[:, 1] = np.random.normal(size=N, scale=20, loc=0)
+
+
+canvas = scene.SceneCanvas(keys='interactive', size=(400, 200), show=True)
+
+# Create a visual that updates the line with different colormaps
+color = next(colormaps)
+line = scene.Line(pos=pos, color=color, method='gl')
+line.transform = STTransform(translate=[0, 140])
+line.parent = canvas.central_widget
+
+text = scene.Text(color, bold=True, font_size=24, color='w',
+                  pos=(200, 40), parent=canvas.central_widget)
+
+
+def on_timer(event):
+    global colormaps, line, text, pos
+    color = next(colormaps)
+    line.set_data(pos=pos, color=color)
+    text.text = color
+
+timer = app.Timer(.5, connect=on_timer, start=True)
+
+
+if __name__ == '__main__':
+    canvas.app.run()
diff --git a/examples/basics/scene/console.py b/examples/basics/scene/console.py
new file mode 100644
index 0000000..18d1aaa
--- /dev/null
+++ b/examples/basics/scene/console.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# vispy: gallery 30
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Demonstrate the use of the vispy console. Note how the console size is
+independent of the canvas scaling.
+"""
+import sys
+
+from vispy import scene, app
+from vispy.scene.widgets import Console
+from vispy.scene.visuals import Text
+
+canvas = scene.SceneCanvas(keys='interactive', size=(400, 400))
+grid = canvas.central_widget.add_grid()
+
+vb = scene.widgets.ViewBox(border_color='b')
+vb.camera = 'panzoom'
+vb.camera.rect = -1, -1, 2, 2
+grid.add_widget(vb, row=0, col=0)
+text = Text('Starting timer...', color='w', font_size=24, parent=vb.scene)
+
+console = Console(text_color='g', font_size=12., border_color='g')
+grid.add_widget(console, row=1, col=0)
+
+
+def on_timer(event):
+    text.text = 'Tick #%s' % event.iteration
+    if event.iteration > 1 and event.iteration % 10 == 0:
+        console.clear()
+    console.write('Elapsed:\n  %s' % event.elapsed)
+    canvas.update()
+
+timer = app.Timer(2.0, connect=on_timer, start=True)
+
+console.write('This is a line that will be wrapped automatically by the '
+              'console.\n')
+console.write('This line will be truncated ....................,\n'
+              'but this next line will survive.\n', wrap=False)
+
+if __name__ == '__main__':
+    canvas.show()
+    if sys.flags.interactive != 1:
+        canvas.app.run()
diff --git a/examples/basics/scene/cube.py b/examples/basics/scene/cube.py
new file mode 100644
index 0000000..d9fdaf4
--- /dev/null
+++ b/examples/basics/scene/cube.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# vispy: gallery 30
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Simple use of SceneCanvas to display a cube with an arcball camera.
+"""
+import sys
+
+from vispy import scene
+
+canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
+
+# Set up a viewbox to display the cube with interactive arcball
+view = canvas.central_widget.add_view()
+cube = scene.visuals.Cube(edge_color='k', parent=view.scene)
+view.camera = 'arcball'
+view.camera.fov = 30.
+
+if __name__ == '__main__' and sys.flags.interactive == 0:
+    canvas.app.run()
diff --git a/examples/basics/scene/flipped_axis.py b/examples/basics/scene/flipped_axis.py
new file mode 100644
index 0000000..f649fb1
--- /dev/null
+++ b/examples/basics/scene/flipped_axis.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+# vispy: gallery 2
+
+"""
+Example demonstrating the use of aspect ratio, and also the flipping
+of axis using negative aspect ratios.
+
+Keys:
+* 1: flip x dimenstion
+* 2: flip y dimension
+* 3: flip z dimenstion
+* 4: cycle through up-vectors
+* 5: cycle through cameras
+"""
+
+from itertools import cycle
+
+import numpy as np
+
+from vispy import app, scene, io
+from vispy.ext.six import next
+
+# Read volume
+vol1 = np.load(io.load_data_file('volume/stent.npz'))['arr_0']
+
+# Prepare canvas
+canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
+canvas.measure_fps()
+
+# Set up a viewbox to display the image with interactive pan/zoom
+view = canvas.central_widget.add_view()
+
+# Create the volume visuals, only one is visible
+volume1 = scene.visuals.Volume(vol1, parent=view.scene, threshold=0.5)
+#volume1.method = 'iso'
+volume1.threshold = 0.1
+
+# Plot a line that shows where positive x is, with at the end a small
+# line pointing at positive y
+arr = np.array([(100, -1, -1), (-1, -1, -1), (-1, 10, -1)])
+line1 = scene.visuals.Line(arr, color='red', parent=view.scene)
+
+# Create cameras
+cam1 = scene.cameras.PanZoomCamera(parent=view.scene, aspect=1)
+cam2 = scene.cameras.FlyCamera(parent=view.scene)
+cam3 = scene.cameras.TurntableCamera(fov=60, parent=view.scene)
+cam4 = scene.cameras.ArcballCamera(fov=60, parent=view.scene)
+cams = (cam1, cam2, cam3, cam4)
+view.camera = cam3  # Select turntable at first
+
+ups = cycle(('+z', '-z', '+y', '-y', '+x', '-x'))
+
+
+# Implement key presses
+ at canvas.events.key_press.connect
+def on_key_press(event):
+    if event.text == '1':
+        for cam in cams:
+            flip = cam.flip
+            cam.flip = not flip[0], flip[1], flip[2]
+    elif event.text == '2':
+        for cam in cams:
+            flip = cam.flip
+            cam.flip = flip[0], not flip[1], flip[2]
+    elif event.text == '3':
+        for cam in cams:
+            flip = cam.flip
+            cam.flip = flip[0], flip[1], not flip[2]
+    elif event.text == '4':
+        up = next(ups)
+        print('up: ' + up)
+        for cam in cams:
+            cam.up = up
+    if event.text == '5':
+        cam_toggle = {cam1: cam2, cam2: cam3, cam3: cam4, cam4: cam1}
+        view.camera = cam_toggle.get(view.camera, 'fly')
+    elif event.text == '0':
+        for cam in cams:
+            cam.set_range()
+
+if __name__ == '__main__':
+    print(__doc__)
+    app.run()
diff --git a/examples/basics/scene/grid.py b/examples/basics/scene/grid.py
index edabb0e..0656421 100644
--- a/examples/basics/scene/grid.py
+++ b/examples/basics/scene/grid.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
@@ -23,18 +23,21 @@ grid = canvas.central_widget.add_grid()
 
 # Add 3 ViewBoxes to the grid
 b1 = grid.add_view(row=0, col=0, col_span=2)
+b1.camera = 'panzoom'
 b1.border_color = (0.5, 0.5, 0.5, 1)
-b1.camera.rect = (-0.5, -5), (11, 10)
+b1.camera = scene.PanZoomCamera(rect=(-0.5, -5, 11, 10))
 b1.border = (1, 0, 0, 1)
 
 b2 = grid.add_view(row=1, col=0)
+b2.camera = 'panzoom'
 b2.border_color = (0.5, 0.5, 0.5, 1)
-b2.camera.rect = (-10, -5), (15, 10)
+b2.camera = scene.PanZoomCamera(rect=(-10, -5, 15, 10))
 b2.border = (1, 0, 0, 1)
 
 b3 = grid.add_view(row=1, col=1)
+b3.camera = 'panzoom'
 b3.border_color = (0.5, 0.5, 0.5, 1)
-b3.camera.rect = (-5, -5), (10, 10)
+b3.camera = scene.PanZoomCamera(rect=(-5, -5, 10, 10))
 b3.border = (1, 0, 0, 1)
 
 
@@ -50,20 +53,20 @@ color[:, 0] = np.linspace(0, 1, N)
 color[:, 1] = color[::-1, 0]
 
 # Top grid cell shows plot data in a rectangular coordinate system.
-l1 = scene.visuals.Line(pos=pos, color=color, antialias=False, mode='gl')
+l1 = scene.visuals.Line(pos=pos, color=color, antialias=False, method='gl')
 b1.add(l1)
 grid1 = scene.visuals.GridLines(parent=b1.scene)
 
 # Bottom-left grid cell shows the same data with log-transformed X
-e2 = scene.Entity(parent=b2.scene)
+e2 = scene.Node(parent=b2.scene)
 e2.transform = scene.transforms.LogTransform(base=(2, 0, 0))
 l2 = scene.visuals.Line(pos=pos, color=color, antialias=False, parent=e2,
-                        mode='gl')
+                        method='gl')
 grid2 = scene.visuals.GridLines(parent=e2)
 
 # Bottom-right grid cell shows the same data again, but with a much more
 # interesting transformation.
-e3 = scene.Entity(parent=b3.scene)
+e3 = scene.Node(parent=b3.scene)
 affine = scene.transforms.AffineTransform()
 affine.scale((1, 0.1))
 affine.rotate(10, (0, 0, 1))
@@ -72,7 +75,7 @@ e3.transform = scene.transforms.ChainTransform([
     scene.transforms.PolarTransform(),
     affine])
 l3 = scene.visuals.Line(pos=pos, color=color, antialias=False, parent=e3,
-                        mode='gl')
+                        method='gl')
 grid3 = scene.visuals.GridLines(scale=(np.pi/6., 1.0), parent=e3)
 
 if __name__ == '__main__' and sys.flags.interactive == 0:
diff --git a/examples/basics/scene/grid_large.py b/examples/basics/scene/grid_large.py
index 564d9ca..4e552e3 100644
--- a/examples/basics/scene/grid_large.py
+++ b/examples/basics/scene/grid_large.py
@@ -1,13 +1,14 @@
 # -*- coding: utf-8 -*-
-# vispy: gallery 2
+# vispy: testskip  # disabled due to segfaults on travis
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
 Test automatic layout of multiple viewboxes using Grid.
 """
 
+import sys
 from vispy import scene
 from vispy import app
 import numpy as np
@@ -25,16 +26,16 @@ for i in range(10):
     lines.append([])
     for j in range(10):
         vb = grid.add_view(row=i, col=j)
+        vb.camera = 'panzoom'
         vb.camera.rect = (0, -5), (100, 10)
         vb.border = (1, 1, 1, 0.4)
 
         pos = np.empty((N, 2), dtype=np.float32)
         pos[:, 0] = np.linspace(0, 100, N)
         pos[:, 1] = np.random.normal(size=N)
-        line = scene.visuals.Line(pos=pos, color=(1, 1, 1, 0.5), mode='gl')
+        line = scene.visuals.Line(pos=pos, color=(1, 1, 1, 0.5), method='gl')
         vb.add(line)
 
 
-import sys
 if __name__ == '__main__' and sys.flags.interactive == 0:
     app.run()
diff --git a/examples/basics/scene/image.py b/examples/basics/scene/image.py
index 8e8c7b1..d975a07 100644
--- a/examples/basics/scene/image.py
+++ b/examples/basics/scene/image.py
@@ -1,12 +1,13 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
 Simple use of SceneCanvas to display an Image.
 """
+import sys
 from vispy import scene
 from vispy import app
 import numpy as np
@@ -23,9 +24,8 @@ img_data = np.random.normal(size=(100, 100, 3), loc=128,
                             scale=50).astype(np.ubyte)
 image = scene.visuals.Image(img_data, parent=view.scene)
 
-# Set the view bounds to show the entire image with some padding
-view.camera.rect = (-10, -10, image.size[0]+20, image.size[1]+20)
+# Set 2D camera (the camera will scale to the contents in the scene)
+view.camera = scene.PanZoomCamera(aspect=1)
 
-import sys
 if __name__ == '__main__' and sys.flags.interactive == 0:
     app.run()
diff --git a/examples/basics/scene/isocurve.py b/examples/basics/scene/isocurve.py
index ec55ea6..cd8466c 100644
--- a/examples/basics/scene/isocurve.py
+++ b/examples/basics/scene/isocurve.py
@@ -1,13 +1,14 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
 Simple use of SceneCanvas to display an Isocurve visual.
 """
-from vispy import app, scene
+import sys
+from vispy import app, scene, visuals
 from vispy.util.filter import gaussian_filter
 import numpy as np
 
@@ -24,20 +25,21 @@ noise = np.random.normal(size=(100, 100), loc=50, scale=150)
 noise = gaussian_filter(noise, (4, 4, 0))
 img_data[:] = noise[..., np.newaxis]
 image = scene.visuals.Image(img_data, parent=view.scene)
+# move image behind curves
+image.transform = visuals.transforms.STTransform(translate=(0, 0, 0.5)) 
 
 # Create isocurve, make a child of the image to ensure the two are always
 # aligned.
-curve1 = scene.visuals.Isocurve(noise, level=60, color=(0, 0, 1, 1), 
+curve1 = scene.visuals.Isocurve(noise, level=60, color=(1, 1, 0, 1), 
                                 parent=view.scene)
-curve2 = scene.visuals.Isocurve(noise, level=50, color=(0, 0, 0.5, 1), 
+curve2 = scene.visuals.Isocurve(noise, level=50, color=(1, 0.5, 0, 1), 
                                 parent=view.scene)
-curve3 = scene.visuals.Isocurve(noise, level=40, color=(0, 0, 0.3, 1), 
+curve3 = scene.visuals.Isocurve(noise, level=40, color=(1, 0, 0, 1), 
                                 parent=view.scene)
 
-# Set the view bounds to show the entire image with some padding
-view.camera.rect = (-10, -10, image.size[0]+20, image.size[1]+20)
+# Set 2D camera (the camera will scale to the contents in the scene)
+view.camera = scene.PanZoomCamera(aspect=1)
 
 
-import sys
 if __name__ == '__main__' and sys.flags.interactive == 0:
     app.run()
diff --git a/examples/basics/scene/isocurve_for_trisurface.py b/examples/basics/scene/isocurve_for_trisurface.py
new file mode 100644
index 0000000..8bf9215
--- /dev/null
+++ b/examples/basics/scene/isocurve_for_trisurface.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+"""
+This example demonstrates isocurve for triangular mesh with vertex data.
+"""
+
+import numpy as np
+
+from vispy import app, scene
+
+from vispy.geometry.generation import create_sphere
+
+import sys
+
+# Create a canvas with a 3D viewport
+canvas = scene.SceneCanvas(keys='interactive')
+canvas.show()
+view = canvas.central_widget.add_view()
+
+cols = 10
+rows = 10
+radius = 2
+nbr_level = 20
+mesh = create_sphere(cols, rows, radius=radius)
+vertices = mesh.get_vertices()
+tris = mesh.get_faces()
+
+cl = np.linspace(-radius, radius, nbr_level+2)[1:-1]
+
+scene.visuals.Isoline(vertices=vertices, tris=tris, data=vertices[:, 2],
+                      levels=cl, color_lev='winter', parent=view.scene)
+
+# Add a 3D axis to keep us oriented
+scene.visuals.XYZAxis(parent=view.scene)
+
+view.camera = scene.TurntableCamera()
+view.camera.set_range((-1, 1), (-1, 1), (-1, 1))
+
+if __name__ == '__main__':
+    canvas.show()
+    if sys.flags.interactive == 0:
+        app.run()
diff --git a/examples/basics/scene/isosurface.py b/examples/basics/scene/isosurface.py
index 36d52ef..ab3ffaf 100644
--- a/examples/basics/scene/isosurface.py
+++ b/examples/basics/scene/isosurface.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
@@ -15,9 +15,7 @@ from vispy import app, scene
 
 # Create a canvas with a 3D viewport
 canvas = scene.SceneCanvas(keys='interactive')
-canvas.show()
 view = canvas.central_widget.add_view()
-view.set_camera('turntable', mode='perspective', up='z', distance=50)
 
 
 ## Define a scalar field from which we will generate an isosurface
@@ -28,16 +26,15 @@ def psi(i, j, k, offset=(25, 25, 50)):
     th = np.arctan2(z, (x**2+y**2)**0.5)
     r = (x**2 + y**2 + z**2)**0.5
     a0 = 1
-    ps = ((1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * 
+    ps = ((1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 *
           np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1))
-    
     return ps
 
 print("Generating scalar field..")
 data = np.abs(np.fromfunction(psi, (50, 50, 100)))
 
 # Create isosurface visual
-surface = scene.visuals.Isosurface(data, level=data.max()/4., 
+surface = scene.visuals.Isosurface(data, level=data.max()/4.,
                                    color=(0.5, 0.6, 1, 1), shading='smooth',
                                    parent=view.scene)
 surface.transform = scene.transforms.STTransform(translate=(-25, -25, -50))
@@ -45,6 +42,15 @@ surface.transform = scene.transforms.STTransform(translate=(-25, -25, -50))
 # Add a 3D axis to keep us oriented
 axis = scene.visuals.XYZAxis(parent=view.scene)
 
+# Use a 3D camera
+# Manual bounds; Mesh visual does not provide bounds yet
+# Note how you can set bounds before assigning the camera to the viewbox
+cam = scene.TurntableCamera(elevation=30, azimuth=30)
+cam.set_range((-10, 10), (-10, 10), (-10, 10))
+view.camera = cam
 
-if sys.flags.interactive == 0:
-    app.run()
+
+if __name__ == '__main__':
+    canvas.show()
+    if sys.flags.interactive == 0:
+        app.run()
diff --git a/examples/basics/scene/line.py b/examples/basics/scene/line.py
index cf31694..6fc9cd2 100644
--- a/examples/basics/scene/line.py
+++ b/examples/basics/scene/line.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
@@ -13,7 +13,7 @@ import numpy as np
 
 from vispy import app, scene
 
-canvas = scene.SceneCanvas(size=(800, 600), show=True, keys='interactive')
+canvas = scene.SceneCanvas(size=(800, 600), keys='interactive')
 
 N = 1000
 pos = np.empty((N, 2), np.float32)
@@ -43,5 +43,7 @@ def update(event):
 
 timer = app.Timer('auto', connect=update, start=True)
 
-if sys.flags.interactive == 0:
-    app.run()
+if __name__ == '__main__':
+    canvas.show()
+    if sys.flags.interactive == 0:
+        app.run()
diff --git a/examples/basics/scene/line_update.py b/examples/basics/scene/line_update.py
new file mode 100644
index 0000000..bf019aa
--- /dev/null
+++ b/examples/basics/scene/line_update.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# vispy: testskip  # disabled due to segfaults on travis
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+Demonstration of animated Line visual.
+"""
+
+import sys
+import numpy as np
+from vispy import app, scene
+
+# vertex positions of data to draw
+N = 200
+pos = np.zeros((N, 2), dtype=np.float32)
+pos[:, 0] = np.linspace(50., 750., N)
+pos[:, 1] = np.random.normal(size=N, scale=100, loc=400)
+
+# color array
+color = np.ones((N, 4), dtype=np.float32)
+color[:, 0] = np.linspace(0, 1, N)
+color[:, 1] = color[::-1, 0]
+
+canvas = scene.SceneCanvas(keys='interactive', size=(800, 800), show=True)
+
+line = scene.Line(pos, color, parent=canvas.scene)
+
+
+def update(ev):
+    global pos, color, line
+    pos[:, 1] = np.random.normal(size=N, scale=100, loc=400)
+    color = np.roll(color, 1, axis=0)
+    line.set_data(pos=pos, color=color)
+
+timer = app.Timer()
+timer.connect(update)
+timer.start(0)
+
+if __name__ == '__main__' and sys.flags.interactive == 0:
+    app.run()
diff --git a/examples/basics/scene/modular_shaders/editor.py b/examples/basics/scene/modular_shaders/editor.py
index 1c6a857..8647e64 100644
--- a/examples/basics/scene/modular_shaders/editor.py
+++ b/examples/basics/scene/modular_shaders/editor.py
@@ -1,4 +1,10 @@
-#-------------------------------------------------------------------------
+# -*- coding: utf-8 -*-
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
 # QScintilla editor
 #
 # Adapted from Eli Bendersky (eliben at gmail.com)
@@ -6,7 +12,8 @@
 #
 # API: http://pyqt.sourceforge.net/Docs/QScintilla2/classQsciScintilla.html
 #
-#-------------------------------------------------------------------------
+"""
+
 import sys
 import re
 from PyQt4.QtCore import *  # noqa
diff --git a/examples/basics/scene/modular_shaders/sandbox.py b/examples/basics/scene/modular_shaders/sandbox.py
index b1c1144..7416844 100644
--- a/examples/basics/scene/modular_shaders/sandbox.py
+++ b/examples/basics/scene/modular_shaders/sandbox.py
@@ -1,6 +1,11 @@
+# -*- coding: utf-8 -*-
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
 """
-Sandbox for experimenting with vispy.scene.shaders
-
+Sandbox for experimenting with vispy.visuals.shaders
 """
 from PyQt4 import QtCore
 from PyQt4.QtGui import *  # noqa
@@ -28,7 +33,7 @@ Instructions:
 """
 
 
-from vispy.scene.shaders import ModularProgram
+from vispy.visuals.shaders import ModularProgram
 
 vertex_shader = "void main() {}"
 fragment_shader = "void main() {}"
@@ -49,7 +54,7 @@ with no definition. By leaving this function undefined, any new function
 definition may be concatenated to the shader.
 """
 
-from vispy.scene.shaders import ModularProgram, Function
+from vispy.visuals.shaders import ModularProgram, Function
 
 # The hook is called 'input_position', and is used to provide the
 # value for gl_Position.
@@ -101,7 +106,7 @@ In this example, an anonymous function is assigned to a hook. When it is
 compiled into the complete program, it is renamed to match the hook.
 """
 
-from vispy.scene.shaders import ModularProgram, Function
+from vispy.visuals.shaders import ModularProgram, Function
 
 vertex_shader = """
 vec4 input_position();
@@ -150,7 +155,7 @@ a real program variable at compile time.
 In the next example, we will see how ModularProgram resolves name conflicts.
 """
 
-from vispy.scene.shaders import ModularProgram, Function
+from vispy.visuals.shaders import ModularProgram, Function
 import numpy as np
 
 vertex_shader = """
@@ -203,7 +208,7 @@ name.
 This example demonstrates dynamic naming of a program variable.
 """
 
-from vispy.scene.shaders import ModularProgram, Function
+from vispy.visuals.shaders import ModularProgram, Function
 import numpy as np
 
 vertex_shader = """
@@ -258,7 +263,7 @@ Function chains are another essential component of shader composition,
 allowing a list of functions to be executed in order.
 """
 
-from vispy.scene.shaders import ModularProgram, Function, FunctionChain
+from vispy.visuals.shaders import ModularProgram, Function, FunctionChain
 
 # Added a new hook to allow any number of functions to be executed
 # after gl_Position is set.
@@ -324,7 +329,7 @@ This is most commonly used for passing vertex positions through a composition
 of transform functions.
 """
 
-from vispy.scene.shaders import ModularProgram, Function, FunctionChain
+from vispy.visuals.shaders import ModularProgram, Function, FunctionChain
 
 
 vertex_shader = """
@@ -387,7 +392,7 @@ to a fragment shader, we will need to introduce some supporting code
 to the vertex shader.
 """
 
-from vispy.scene.shaders import (ModularProgram, Function, FunctionChain)
+from vispy.visuals.shaders import (ModularProgram, Function, FunctionChain)
 from vispy.gloo import VertexBuffer
 import numpy as np
 
@@ -448,7 +453,7 @@ FRAGMENT = program.frag_code
 """
 """
 
-from vispy.scene.shaders import (ModularProgram, Function, FunctionChain)
+from vispy.visuals.shaders import (ModularProgram, Function, FunctionChain)
 from vispy.gloo import VertexBuffer
 import numpy as np
 
@@ -590,14 +595,17 @@ def update():
         vert = glob['VERTEX']
         frag = glob['FRAGMENT']
         editor.clear_marker()
-    except:
+    except Exception:
         vert = traceback.format_exc()
         frag = ""
         tb = sys.exc_info()[2]
         while tb is not None:
             #print(tb.tb_lineno, tb.tb_frame.f_code.co_filename)
-            if tb.tb_frame.f_code.co_filename == '<string>':
-                editor.set_marker(tb.tb_lineno-1)
+            try:
+                if tb.tb_frame.f_code.co_filename == '<string>':
+                    editor.set_marker(tb.tb_lineno-1)
+            except Exception:
+                pass
             tb = tb.tb_next
 
     vertex.setText(vert)
@@ -606,4 +614,5 @@ def update():
 editor.textChanged.connect(update)
 update()
 
-app.exec_()
+if __name__ == '__main__':
+    app.exec_()
diff --git a/examples/basics/scene/nested_viewbox.py b/examples/basics/scene/nested_viewbox.py
index 3b183bd..daf342f 100644
--- a/examples/basics/scene/nested_viewbox.py
+++ b/examples/basics/scene/nested_viewbox.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # vispy: gallery 2
@@ -82,6 +82,7 @@ vb1 = scene.widgets.ViewBox(parent=canvas.scene, name='vb1',
                             margin=2, border_color='red')
 vb1.pos = 0, 0
 vb1.size = w2, h
+vb1.camera = 'panzoom'
 vb1.camera.rect = (0, 0, 1, 1)
 vb1.camera.interactive = False
 
@@ -90,17 +91,19 @@ vb11 = scene.widgets.ViewBox(parent=vb1.scene, name='vb11',
                              margin=0.02, border_color='green')
 vb11.pos = 0, 0
 vb11.size = 1, 0.5
+vb11.camera = 'panzoom'
 vb11.camera.rect = (0, 0, 1, 1)
-line11 = scene.visuals.Line(pos=pos, color=color, mode='gl', parent=vb11.scene)
+line11 = scene.visuals.Line(pos=pos, color=color, method='gl', 
+                            parent=vb11.scene)
 
 # top-left (+y up)
 vb12 = scene.widgets.ViewBox(parent=vb1.scene, name='vb12', 
                              margin=0.02, border_color='blue')
 vb12.pos = 0, 0.5
 vb12.size = 1, 0.5
-vb12.set_camera(None)  # use parent cs
+vb12.camera = 'base'  # use parent cs
 # vb12 does not apply any scaling, so we do that manually here to match vb11
-line12 = scene.visuals.Line(pos=pos * [[1.0, 0.5]], color=color, mode='gl', 
+line12 = scene.visuals.Line(pos=pos * [[1.0, 0.5]], color=color, method='gl', 
                             parent=vb12.scene)
 
 
@@ -113,7 +116,7 @@ vb2 = scene.widgets.ViewBox(parent=canvas.scene, name='vb2',
                             margin=2, border_color='yellow')
 vb2.pos = w2, 0
 vb2.size = w2, h
-vb2.set_camera(None)
+vb2.camera = 'base'
 vb2.camera.interactive = False
 
 # top-right (+y up)
@@ -121,25 +124,27 @@ vb21 = scene.widgets.ViewBox(parent=vb2.scene, name='vb21',
                              margin=10, border_color='purple')
 vb21.pos = 0, 0
 vb21.size = w2, h2
+vb21.camera = 'panzoom'
 vb21.camera.rect = (0, 0, 1, 1)
-line21 = scene.visuals.Line(pos=pos, color=color, mode='gl', parent=vb21.scene)
+line21 = scene.visuals.Line(pos=pos, color=color, method='gl', 
+                            parent=vb21.scene)
 
 # bottom-right (+y down)
 vb22 = scene.widgets.ViewBox(parent=vb2.scene, name='vb22', 
                              margin=10, border_color='teal')
 vb22.pos = 0, h2
 vb22.size = w2, h2
-vb22.set_camera(None)  # use parent cs
+vb22.camera = 'base'  # use parent cs
 # vb22 does not apply any scaling, so we do that manually here to match vb21
-line22 = scene.visuals.Line(pos=pos * [[w2, h2]], color=color, mode='gl', 
+line22 = scene.visuals.Line(pos=pos * [[w2, h2]], color=color, method='gl', 
                             parent=vb22.scene)
 
 
 # Set preferred clipping methods
 for vb in [vb1, vb11, vb21]:
-    vb.preferred_clip_method = CLIP_METHOD1
+    vb.clip_method = CLIP_METHOD1
 for vb in [vb2, vb12, vb22]:
-    vb.preferred_clip_method = CLIP_METHOD2
+    vb.clip_method = CLIP_METHOD2
 
 
 if __name__ == '__main__':
diff --git a/examples/basics/scene/one_cam_two_scenes.py b/examples/basics/scene/one_cam_two_scenes.py
new file mode 100644
index 0000000..95c9d4a
--- /dev/null
+++ b/examples/basics/scene/one_cam_two_scenes.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+# vispy: gallery 2
+
+"""
+Demonstrating two scenes that share the same camera view by linking the
+cameras.
+"""
+
+import numpy as np
+
+from vispy import app, scene, io
+
+canvas = scene.SceneCanvas(keys='interactive')
+canvas.size = 800, 600
+canvas.show()
+
+# Create two ViewBoxes, place side-by-side
+vb1 = scene.widgets.ViewBox(border_color='yellow', parent=canvas.scene)
+vb2 = scene.widgets.ViewBox(border_color='blue', parent=canvas.scene)
+#
+grid = canvas.central_widget.add_grid()
+grid.padding = 6
+grid.add_widget(vb1, 0, 0)
+grid.add_widget(vb2, 0, 1)
+
+# Create the image
+im1 = io.load_crate().astype('float32') / 255
+# Make gray, smooth, and take derivatives: edge enhancement
+im2 = im1[:, :, 1]
+im2 = (im2[1:-1, 1:-1] + im2[0:-2, 1:-1] + im2[2:, 1:-1] + 
+       im2[1:-1, 0:-2] + im2[1:-1, 2:]) / 5
+im2 = 0.5 + (np.abs(im2[0:-2, 1:-1] - im2[1:-1, 1:-1]) + 
+             np.abs(im2[1:-1, 0:-2] - im2[1:-1, 1:-1]))
+
+image1 = scene.visuals.Image(im1, parent=vb1.scene)
+image2 = scene.visuals.Image(im2, parent=vb2.scene)
+
+# Set 2D camera (PanZoomCamera, TurnTableCamera)
+vb1.camera, vb2.camera = scene.PanZoomCamera(), scene.PanZoomCamera()
+vb1.camera.aspect = vb2.camera.aspect = 1  # no auto-scale
+vb1.camera.link(vb2.camera)
+
+# Set the view bounds to show the entire image with some padding
+#view.camera.rect = (-10, -10, image.size[0]+20, image.size[1]+20)
+
+if __name__ == '__main__':
+    app.run()
diff --git a/examples/basics/scene/one_scene_four_cams.py b/examples/basics/scene/one_scene_four_cams.py
new file mode 100644
index 0000000..a5726ca
--- /dev/null
+++ b/examples/basics/scene/one_scene_four_cams.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+# vispy: gallery 2
+
+"""
+Demonstrating a single scene that is shown in four different viewboxes,
+each with a different camera.
+"""
+
+# todo: the panzoom camera sometimes work, sometimes not. Not sure why.
+# we should probably make iterating over children deterministic, so that
+# an error like this becomes easier to reproduce ...
+
+import sys
+
+from vispy import app, scene, io
+
+canvas = scene.SceneCanvas(keys='interactive')
+canvas.size = 800, 600
+canvas.show()
+
+# Create two ViewBoxes, place side-by-side
+vb1 = scene.widgets.ViewBox(border_color='white', parent=canvas.scene)
+vb2 = scene.widgets.ViewBox(border_color='white', parent=canvas.scene)
+vb3 = scene.widgets.ViewBox(border_color='white', parent=canvas.scene)
+vb4 = scene.widgets.ViewBox(border_color='white', parent=canvas.scene)
+scenes = vb1.scene, vb2.scene, vb3.scene, vb4.scene
+
+# Put viewboxes in a grid
+grid = canvas.central_widget.add_grid()
+grid.padding = 6
+grid.add_widget(vb1, 0, 0)
+grid.add_widget(vb2, 0, 1)
+grid.add_widget(vb3, 1, 0)
+grid.add_widget(vb4, 1, 1)
+
+# Create some visuals to show
+# AK: Ideally, we could just create one visual that is present in all
+# scenes, but that results in flicker for the PanZoomCamera, I suspect
+# due to errors in transform caching.
+im1 = io.load_crate().astype('float32') / 255
+#image1 = scene.visuals.Image(im1, grid=(20, 20), parent=scenes)
+for par in scenes:
+    image = scene.visuals.Image(im1, grid=(20, 20), parent=par)
+
+#vol1 = np.load(io.load_data_file('volume/stent.npz'))['arr_0']
+#volume1 = scene.visuals.Volume(vol1, parent=scenes)
+#volume1.transform = scene.STTransform(translate=(0, 0, 10))
+
+# Assign cameras
+vb1.camera = scene.BaseCamera()
+vb2.camera = scene.PanZoomCamera()
+vb3.camera = scene.TurntableCamera()
+vb4.camera = scene.FlyCamera()
+
+
+# If True, show a cuboid at each camera
+if False:
+    cube = scene.visuals.Cube((3, 3, 5))
+    cube.transform = scene.STTransform(translate=(0, 0, 6))
+    for vb in (vb1, vb2, vb3, vb4):
+        vb.camera.parents = scenes
+        cube.add_parent(vb.camera)
+
+if __name__ == '__main__':
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/examples/basics/scene/sensitivity.py b/examples/basics/scene/sensitivity.py
new file mode 100644
index 0000000..0219aa3
--- /dev/null
+++ b/examples/basics/scene/sensitivity.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+# For testing zoom sensitivity on various platforms
+
+import numpy as np
+import vispy.scene
+from vispy.scene import visuals
+
+canvas = vispy.scene.SceneCanvas(keys='interactive', show=True)
+vb = canvas.central_widget.add_view()
+vb.camera = 'panzoom'
+vb.camera.rect = (-10, -10, 20, 20)
+
+centers = np.random.normal(size=(50, 2))
+pos = np.random.normal(size=(100000, 2), scale=0.2)
+indexes = np.random.normal(size=100000, loc=centers.shape[0]/2., 
+                           scale=centers.shape[0]/3.)
+indexes = np.clip(indexes, 0, centers.shape[0]-1).astype(int)
+scales = 10**(np.linspace(-2, 0.5, centers.shape[0]))[indexes][:, np.newaxis]
+pos *= scales
+pos += centers[indexes]
+
+scatter = visuals.Markers()
+scatter.set_gl_state('translucent', depth_test=False)
+scatter.set_data(pos, edge_width=0, face_color=(1, 1, 1, 0.3), size=5)
+vb.add(scatter)
+
+
+ at canvas.connect
+def on_key_press(ev):
+    if ev.key.name in '+=':
+        vb.camera.zoom_factor *= 1.1
+    elif ev.key.name == '-':
+        vb.camera.zoom_factor /= 1.1
+    print("Zoom factor: %0.4f" % vb.camera.zoom_factor)
+
+
+if __name__ == '__main__':
+    import sys
+    if sys.flags.interactive != 1:
+        vispy.app.run()
diff --git a/examples/basics/scene/shared_context.py b/examples/basics/scene/shared_context.py
new file mode 100644
index 0000000..3df3095
--- /dev/null
+++ b/examples/basics/scene/shared_context.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+This example demonstrates the use of multiple canvases with visuals shared 
+between them.
+"""
+
+import sys
+import numpy as np
+
+from vispy import app, scene
+from vispy.util.filter import gaussian_filter
+
+
+canvas1 = scene.SceneCanvas(keys='interactive', show=True)
+view1 = canvas1.central_widget.add_view()
+view1.camera = scene.TurntableCamera(fov=60)
+
+canvas2 = scene.SceneCanvas(keys='interactive', show=True, 
+                            shared=canvas1.context)
+view2 = canvas2.central_widget.add_view()
+view2.camera = 'panzoom'
+
+# Simple surface plot example
+# x, y values are not specified, so assumed to be 0:50
+z = gaussian_filter(np.random.normal(size=(50, 50)), (1, 1)) * 10
+p1 = scene.visuals.SurfacePlot(z=z, color=(0.5, 0.5, 1, 1), shading='smooth')
+p1.transform = scene.transforms.AffineTransform()
+p1.transform.scale([1/49., 1/49., 0.02])
+p1.transform.translate([-0.5, -0.5, 0])
+
+view1.add(p1)
+view2.add(p1)
+
+# Add a 3D axis to keep us oriented
+axis = scene.visuals.XYZAxis(parent=view1.scene)
+
+canvas = canvas1  # allow running this example in our test suite
+
+if __name__ == '__main__':
+    if sys.flags.interactive == 0:
+        app.run()
diff --git a/examples/basics/scene/stereo.py b/examples/basics/scene/stereo.py
new file mode 100644
index 0000000..e1b9b76
--- /dev/null
+++ b/examples/basics/scene/stereo.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+"""
+Example demonstrating stereo vision in a scene has an anisotropic aspect
+ratio. This example can be used to test that the cameras behave
+correctly with nested translated/rotated cameras.
+"""
+
+import numpy as np
+
+from vispy import app, scene, io
+
+# Read volume
+vol1 = np.load(io.load_data_file('volume/stent.npz'))['arr_0']
+
+# Prepare canvas
+canvas = scene.SceneCanvas(keys='interactive')
+canvas.size = 800, 600
+canvas.show()
+canvas.measure_fps()
+
+# Set up a viewbox to display the image with interactive pan/zoom
+# Create two ViewBoxes, place side-by-side
+vb1 = scene.widgets.ViewBox(border_color='yellow', parent=canvas.scene)
+vb2 = scene.widgets.ViewBox(border_color='blue', parent=canvas.scene)
+
+# This is temporarily needed because fragment clipping method is not yet
+# compatible with multiple parenting.
+vb1.clip_method = 'viewport'
+vb2.clip_method = 'viewport'
+
+scenes = vb1.scene, vb2.scene
+#
+grid = canvas.central_widget.add_grid()
+grid.padding = 6
+grid.add_widget(vb1, 0, 0)
+grid.add_widget(vb2, 0, 1)
+
+# Create the volume visuals, only one is visible
+volume1 = scene.visuals.Volume(vol1, parent=scenes, threshold=0.5)
+
+# Create cameras. The second is a child of the first, thus inheriting
+# its transform.
+cam1 = scene.cameras.TurntableCamera(parent=scenes, fov=60)
+cam2 = scene.cameras.PerspectiveCamera(parent=cam1, fov=60)
+#
+cam2.transform.translate((+10, 0, 0))
+
+vb1.camera = cam1
+vb2.camera = cam2
+
+if __name__ == '__main__':
+    app.run()
diff --git a/examples/basics/scene/surface_plot.py b/examples/basics/scene/surface_plot.py
index e0c66ca..09e06dc 100644
--- a/examples/basics/scene/surface_plot.py
+++ b/examples/basics/scene/surface_plot.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
@@ -16,12 +16,11 @@ from vispy.util.filter import gaussian_filter
 
 
 canvas = scene.SceneCanvas(keys='interactive')
-canvas.show()
 view = canvas.central_widget.add_view()
-view.set_camera('turntable', mode='perspective', up='z', distance=2)
+view.camera = scene.TurntableCamera(up='z')
 
-## Simple surface plot example
-## x, y values are not specified, so assumed to be 0:50
+# Simple surface plot example
+# x, y values are not specified, so assumed to be 0:50
 z = gaussian_filter(np.random.normal(size=(50, 50)), (1, 1)) * 10
 p1 = scene.visuals.SurfacePlot(z=z, color=(0.5, 0.5, 1, 1), shading='smooth')
 p1.transform = scene.transforms.AffineTransform()
@@ -33,5 +32,7 @@ view.add(p1)
 # Add a 3D axis to keep us oriented
 axis = scene.visuals.XYZAxis(parent=view.scene)
 
-if sys.flags.interactive == 0:
-    app.run()
+if __name__ == '__main__':
+    canvas.show()
+    if sys.flags.interactive == 0:
+        app.run()
diff --git a/examples/basics/scene/text.py b/examples/basics/scene/text.py
index cb0de27..5315edd 100644
--- a/examples/basics/scene/text.py
+++ b/examples/basics/scene/text.py
@@ -1,19 +1,24 @@
-# !/usr/bin/env python
 # -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
 
 """
 Demonstrate the use of text in the root scene and a viewbox. Note
 how the point size is independent of scaling of viewbox and canvas.
 """
-
+import sys
 import numpy as np
-import vispy
+
 from vispy import scene
 from vispy.scene.visuals import Text
 
 # Create canvas with a viewbox at the lower half
 canvas = scene.SceneCanvas(keys='interactive')
 vb = scene.widgets.ViewBox(parent=canvas.scene, border_color='b')
+vb.camera = scene.TurntableCamera(elevation=30, azimuth=30, up='+z')
+axis = scene.visuals.XYZAxis(parent=vb.scene)
 vb.camera.rect = 0, 0, 1, 1
 
 
@@ -36,8 +41,9 @@ N = 1000
 linedata = np.empty((N, 2), np.float32)
 linedata[:, 0] = np.linspace(0, 1, N)
 linedata[:, 1] = np.random.uniform(0.5, 0.1, (N,))
-vispy.scene.visuals.Line(pos=linedata, color='#f006', mode='gl', 
-                         parent=vb.scene)
+scene.visuals.Line(pos=linedata, color='#f006', method='gl', parent=vb.scene)
 
-canvas.show()
-canvas.app.run()
+if __name__ == '__main__':
+    canvas.show()
+    if sys.flags.interactive != 1:
+        canvas.app.run()
diff --git a/examples/basics/scene/viewbox.py b/examples/basics/scene/viewbox.py
index f692382..900206f 100644
--- a/examples/basics/scene/viewbox.py
+++ b/examples/basics/scene/viewbox.py
@@ -1,14 +1,11 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
 Demonstrate ViewBox using various clipping methods.
-
-Two boxes are manually positioned on the canvas; they are not updated
-when the canvas resizes.
 """
 import sys
 import numpy as np
@@ -19,33 +16,24 @@ from vispy import scene
 
 # Create canvas
 canvas = scene.SceneCanvas(size=(800, 600), show=True, keys='interactive')
-
 grid = canvas.central_widget.add_grid()
 
 # Create two ViewBoxes, place side-by-side
+vb1 = grid.add_view(name='vb1', border_color='yellow')
+# Viewboxes can use one of 3 different clipping methods: 'fragment', 
+# 'viewport', or 'fbo'. The default is 'fragment', which does all clipping in
+# the fragment shader.
+vb1.clip_method = 'fragment'
 # First ViewBox uses a 2D pan/zoom camera
-vb1 = scene.widgets.ViewBox(name='vb1', border_color='yellow', parent=grid)
-vb1.clip_method = 'fbo'
-vb1.camera.rect = (-1.2, -2, 2.4, 4)
+vb1.camera = 'panzoom'
 
-# Second ViewBox uses a 3D orthographic camera
-vb2 = scene.widgets.ViewBox(name='vb2', border_color='blue', parent=grid)
+# Second ViewBox uses a 3D perspective camera
+vb2 = grid.add_view(name='vb2', border_color='yellow')
 vb2.parent = canvas.scene
+# Second ViewBox uses glViewport to implement clipping and a 3D turntable
+# camera.
 vb2.clip_method = 'viewport'
-vb2.set_camera('turntable', mode='ortho', elevation=30, azimuth=30, up='y')
-#vb2.set_camera('turntable', mode='perspective',
-#               distance=10, elevation=0, azimuth=0)
-
-
-# Move these when the canvas changes size
- at canvas.events.resize.connect
-def resize(event=None):
-    vb1.pos = 20, 20
-    vb1.size = canvas.size[0]/2. - 40, canvas.size[1] - 40
-    vb2.pos = canvas.size[0]/2. + 20, 20
-    vb2.size = canvas.size[0]/2. - 40, canvas.size[1] - 40
-
-resize()
+vb2.camera = scene.TurntableCamera(elevation=30, azimuth=30, up='+y')
 
 
 #
@@ -64,10 +52,10 @@ pos[:, 1] = np.random.normal(0.0, 0.5, size=N)
 pos[:20, 1] = -0.5  # So we can see which side is down
 
 # make a single plot line and display in both viewboxes
-line1 = scene.visuals.Line(pos=pos.copy(), color=color, mode='gl',
+line1 = scene.visuals.Line(pos=pos.copy(), color=color, method='gl',
                            antialias=False, name='line1', parent=vb1.scene)
-line1.add_parent(vb1.scene)
-line1.add_parent(vb2.scene)
+line2 = scene.visuals.Line(pos=pos.copy(), color=color, method='gl',
+                           antialias=False, name='line1', parent=vb2.scene)
 
 
 # And some squares:
@@ -79,19 +67,21 @@ box = np.array([[0, 0, 0],
 z = np.array([[0, 0, 1]], dtype=np.float32)
 
 # First two boxes are added to both views
-box1 = scene.visuals.Line(pos=box, color=(0.7, 0, 0, 1), mode='gl',
+box1 = scene.visuals.Line(pos=box, color=(0.7, 0, 0, 1), method='gl',
                           name='unit box', parent=vb1.scene)
-box1.add_parent(vb2.scene)
+box2 = scene.visuals.Line(pos=box, color=(0.7, 0, 0, 1), method='gl',
+                          name='unit box', parent=vb2.scene)
 
-box2 = scene.visuals.Line(pos=(box * 2 - 1),  color=(0, 0.7, 0, 1), mode='gl',
-                          name='nd box', parent=vb1.scene)
-box2.add_parent(vb2.scene)
+box2 = scene.visuals.Line(pos=(box * 2 - 1),  color=(0, 0.7, 0, 1),
+                          method='gl', name='nd box', parent=vb1.scene)
+box3 = scene.visuals.Line(pos=(box * 2 - 1),  color=(0, 0.7, 0, 1),
+                          method='gl', name='nd box', parent=vb2.scene)
 
 # These boxes are only added to the 3D view.
-box3 = scene.visuals.Line(pos=box + z, color=(1, 0, 0, 1), mode='gl',
-                          name='unit box', parent=vb2.scene)
-box4 = scene.visuals.Line(pos=((box + z) * 2 - 1), color=(0, 1, 0, 1),
-                          mode='gl', name='nd box', parent=vb2.scene)
+box3 = scene.visuals.Line(pos=box + z, color=(1, 0, 0, 1),
+                          method='gl', name='unit box', parent=vb2.scene)
+box5 = scene.visuals.Line(pos=((box + z) * 2 - 1), color=(0, 1, 0, 1),
+                          method='gl', name='nd box', parent=vb2.scene)
 
 
 if __name__ == '__main__' and sys.flags.interactive == 0:
diff --git a/examples/basics/scene/volume.py b/examples/basics/scene/volume.py
new file mode 100644
index 0000000..2391991
--- /dev/null
+++ b/examples/basics/scene/volume.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+# vispy: gallery 2
+
+"""
+Example volume rendering
+
+Controls:
+
+* 1  - toggle camera between first person (fly) and regular 3D (turntable)
+* 2  - toggle between volume rendering methods
+* 3  - toggle between stent-CT / brain-MRI image
+* 4  - toggle between colormaps
+* 0  - reset cameras
+* [] - decrease/increase isosurface threshold
+
+With fly camera:
+
+* WASD or arrow keys - move around
+* SPACE - brake
+* FC - move up-down
+* IJKL or mouse - look around
+"""
+
+from itertools import cycle
+
+import numpy as np
+
+from vispy import app, scene, io
+from vispy.color import get_colormaps, BaseColormap
+
+# Read volume
+vol1 = np.load(io.load_data_file('volume/stent.npz'))['arr_0']
+vol2 = np.load(io.load_data_file('brain/mri.npz'))['data']
+vol2 = np.flipud(np.rollaxis(vol2, 1))
+
+# Prepare canvas
+canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
+canvas.measure_fps()
+
+# Set up a viewbox to display the image with interactive pan/zoom
+view = canvas.central_widget.add_view()
+
+# Set whether we are emulating a 3D texture
+emulate_texture = False
+
+# Create the volume visuals, only one is visible
+volume1 = scene.visuals.Volume(vol1, parent=view.scene, threshold=0.225,
+                               emulate_texture=emulate_texture)
+volume1.transform = scene.STTransform(translate=(64, 64, 0))
+volume2 = scene.visuals.Volume(vol2, parent=view.scene, threshold=0.2,
+                               emulate_texture=emulate_texture)
+volume2.visible = False
+
+# Create two cameras (1 for firstperson, 3 for 3d person)
+fov = 60.
+cam1 = scene.cameras.FlyCamera(parent=view.scene, fov=fov)
+cam2 = scene.cameras.TurntableCamera(parent=view.scene, fov=fov)
+cam3 = scene.cameras.ArcballCamera(parent=view.scene, fov=fov)
+view.camera = cam2  # Select turntable at first
+
+
+# create colormaps that work well for translucent and additive volume rendering
+class TransFire(BaseColormap):
+    glsl_map = """
+    vec4 translucent_fire(float t) {
+        return vec4(pow(t, 0.5), t, t*t, max(0, t*1.05 - 0.05));
+    }
+    """
+
+
+class TransGrays(BaseColormap):
+    glsl_map = """
+    vec4 translucent_grays(float t) {
+        return vec4(t, t, t, t*0.05);
+    }
+    """
+
+# Setup colormap iterators
+opaque_cmaps = cycle(get_colormaps())
+translucent_cmaps = cycle([TransFire(), TransGrays()])
+opaque_cmap = next(opaque_cmaps)
+translucent_cmap = next(translucent_cmaps)
+
+
+# Implement key presses
+ at canvas.events.key_press.connect
+def on_key_press(event):
+    global opaque_cmap, translucent_cmap
+    if event.text == '1':
+        cam_toggle = {cam1: cam2, cam2: cam3, cam3: cam1}
+        view.camera = cam_toggle.get(view.camera, 'fly')
+    elif event.text == '2':
+        methods = ['mip', 'translucent', 'iso', 'additive']
+        method = methods[(methods.index(volume1.method) + 1) % 4]
+        print("Volume render method: %s" % method)
+        cmap = opaque_cmap if method in ['mip', 'iso'] else translucent_cmap
+        volume1.method = method
+        volume1.cmap = cmap
+        volume2.method = method
+        volume2.cmap = cmap
+    elif event.text == '3':
+        volume1.visible = not volume1.visible
+        volume2.visible = not volume1.visible
+    elif event.text == '4':
+        if volume1.method in ['mip', 'iso']:
+            cmap = opaque_cmap = next(opaque_cmaps)
+        else:
+            cmap = translucent_cmap = next(translucent_cmaps)
+        volume1.cmap = cmap
+        volume2.cmap = cmap
+    elif event.text == '0':
+        cam1.set_range()
+        cam3.set_range()
+    elif event.text != '' and event.text in '[]':
+        s = -0.025 if event.text == '[' else 0.025
+        volume1.threshold += s
+        volume2.threshold += s
+        th = volume1.threshold if volume1.visible else volume2.threshold
+        print("Isosurface threshold: %0.3f" % th)
+
+
+# for testing performance
+#@canvas.connect
+#def on_draw(ev):
+    #canvas.update()
+
+if __name__ == '__main__':
+    print(__doc__)
+    app.run()
diff --git a/examples/basics/visuals/arcball.py b/examples/basics/visuals/arcball.py
new file mode 100644
index 0000000..2d785df
--- /dev/null
+++ b/examples/basics/visuals/arcball.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+Demonstration of how to interact with visuals, here with simple
+arcball-style control.
+"""
+
+import sys
+import numpy as np
+
+from vispy import app, gloo
+from vispy.visuals import CubeVisual, transforms
+from vispy.util.quaternion import Quaternion
+
+
+class Canvas(app.Canvas):
+    def __init__(self):
+        app.Canvas.__init__(self, 'Cube', keys='interactive',
+                            size=(400, 400))
+
+        self.cube = CubeVisual((1.0, 0.5, 0.25), color='red',
+                               edge_color='black')
+        self.quaternion = Quaternion()
+
+        # Create a TransformSystem that will tell the visual how to draw
+        self.cube_transform = transforms.AffineTransform()
+        self.cube_transform.scale((100, 100, 0.001))
+        self.cube_transform.translate((200, 200))
+        self.tr_sys = transforms.TransformSystem(self)
+        self.tr_sys.visual_to_document = self.cube_transform
+        self.show()
+
+    def on_draw(self, event):
+        gloo.set_viewport(0, 0, *self.physical_size)
+        gloo.clear('white')
+        self.tr_sys.auto_configure()
+        self.cube.draw(self.tr_sys)
+
+    def on_mouse_move(self, event):
+        if event.button == 1 and event.last_event is not None:
+            x0, y0 = event.last_event.pos
+            x1, y1 = event.pos
+            w, h = self.size
+            self.quaternion = (self.quaternion *
+                               Quaternion(*_arcball(x0, y0, w, h)) *
+                               Quaternion(*_arcball(x1, y1, w, h)))
+            self.cube_transform.matrix = self.quaternion.get_matrix()
+            self.cube_transform.scale((100, 100, 0.001))
+            self.cube_transform.translate((200, 200))
+            self.update()
+
+
+def _arcball(x, y, w, h):
+    """Convert x,y coordinates to w,x,y,z Quaternion parameters
+
+    Adapted from:
+
+    linalg library
+
+    Copyright (c) 2010-2015, Renaud Blanch <rndblnch at gmail dot com>
+    Licence at your convenience:
+    GPLv3 or higher <http://www.gnu.org/licenses/gpl.html>
+    BSD new <http://opensource.org/licenses/BSD-3-Clause>
+    """
+    r = (w + h) / 2.
+    x, y = -(2. * x - w) / r, -(2. * y - h) / r
+    h = np.sqrt(x*x + y*y)
+    return (0., x/h, y/h, 0.) if h > 1. else (0., x, y, np.sqrt(1. - h*h))
+
+if __name__ == '__main__':
+    win = Canvas()
+    win.show()
+    if sys.flags.interactive != 1:
+        win.app.run()
diff --git a/examples/basics/visuals/cube.py b/examples/basics/visuals/cube.py
new file mode 100644
index 0000000..6e1d2da
--- /dev/null
+++ b/examples/basics/visuals/cube.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+Demonstration of Cube
+"""
+
+import sys
+
+from vispy import app, gloo
+from vispy.visuals import CubeVisual, transforms
+
+
+class Canvas(app.Canvas):
+    def __init__(self):
+        app.Canvas.__init__(self, 'Cube', keys='interactive',
+                            size=(400, 400))
+
+        self.cube = CubeVisual((1.0, 0.5, 0.25), color='red',
+                               edge_color='black')
+        self.theta = 0
+        self.phi = 0
+
+        # Create a TransformSystem that will tell the visual how to draw
+        self.cube_transform = transforms.AffineTransform()
+        self.tr_sys = transforms.TransformSystem(self)
+        self.tr_sys.visual_to_document = self.cube_transform
+
+        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
+
+        self.show()
+
+    def on_draw(self, event):
+        gloo.set_viewport(0, 0, *self.physical_size)
+        gloo.clear('white')
+        self.tr_sys.auto_configure()
+        self.cube.draw(self.tr_sys)
+
+    def on_timer(self, event):
+        self.theta += .5
+        self.phi += .5
+        self.cube_transform.reset()
+        self.cube_transform.rotate(self.theta, (0, 0, 1))
+        self.cube_transform.rotate(self.phi, (0, 1, 0))
+        self.cube_transform.scale((100, 100, 0.001))
+        self.cube_transform.translate((200, 200))
+        self.update()
+
+if __name__ == '__main__':
+    win = Canvas()
+    win.show()
+    if sys.flags.interactive != 1:
+        win.app.run()
diff --git a/examples/basics/visuals/custom_visual.py b/examples/basics/visuals/custom_visual.py
index 09c1d20..bf82130 100644
--- a/examples/basics/visuals/custom_visual.py
+++ b/examples/basics/visuals/custom_visual.py
@@ -1,42 +1,24 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 from __future__ import division
 import numpy as np
-from math import exp
 
 from vispy import app
 from vispy import gloo
-from vispy.scene.shaders import ModularProgram
-from vispy.scene.visuals import Visual
-from vispy.scene.transforms import STTransform, LogTransform
-
-
-class PanZoomTransform(STTransform):
-    def move(self, dx):
-        """I call this when I want to translate."""
-        dx, dy = dx
-        self.translate = (self.translate[0] + dx/self.scale[0],
-                          self.translate[1] + dy/self.scale[1])
-
-    def zoom(self, dx, center=(0., 0.)):
-        """I call this when I want to zoom."""
-        dx, dy = dx
-        scale = (self.scale[0] * exp(2.5*dx),
-                 self.scale[1] * exp(2.5*dy))
-        tr = self.translate
-        self.translate = (tr[0] - center[0] * (1./self.scale[0] - 1./scale[0]),
-                          tr[1] + center[1] * (1./self.scale[1] - 1./scale[1]))
-        self.scale = scale
+from vispy.visuals.shaders import ModularProgram
+from vispy.visuals import Visual
+from vispy.visuals.transforms import (STTransform, LogTransform,
+                                      TransformSystem, ChainTransform)
 
 
 class MarkerVisual(Visual):
     # My full vertex shader, with just a `transform` hook.
     VERTEX_SHADER = """
         #version 120
-        
+
         attribute vec2 a_position;
         attribute vec3 a_color;
         attribute float a_size;
@@ -53,9 +35,9 @@ class MarkerVisual(Visual):
             v_antialias = 1.0;
             v_fg_color  = vec4(0.0,0.0,0.0,0.5);
             v_bg_color  = vec4(a_color,    1.0);
-            
+
             gl_Position = $transform(vec4(a_position,0,1));
-            
+
             gl_PointSize = 2.0*(v_radius + v_linewidth + 1.5*v_antialias);
         }
     """
@@ -86,103 +68,83 @@ class MarkerVisual(Visual):
             }
         }
     """
-    
+
     def __init__(self, pos=None, color=None, size=None):
-        self._program = ModularProgram(self.VERTEX_SHADER, 
+        self._program = ModularProgram(self.VERTEX_SHADER,
                                        self.FRAGMENT_SHADER)
         self.set_data(pos=pos, color=color, size=size)
-        
+
     def set_options(self):
         """Special function that is used to set the options. Automatically
         called at initialization."""
-        gloo.set_state(clear_color=(1, 1, 1, 1), blend=True, 
+        gloo.set_state(clear_color=(1, 1, 1, 1), blend=True,
                        blend_func=('src_alpha', 'one_minus_src_alpha'))
 
     def set_data(self, pos=None, color=None, size=None):
         """I'm not required to use this function. We could also have a system
         of trait attributes, such that a user doing
-        `visual.position = myndarray` results in an automatic update of the 
+        `visual.position = myndarray` results in an automatic update of the
         buffer. Here I just set the buffers manually."""
         self._pos = pos
         self._color = color
         self._size = size
-        
-    def draw(self):
-        # attributes / uniforms are not available until program is built        
+
+    def draw(self, transforms):
+        # attributes / uniforms are not available until program is built
+        tr = transforms.get_full_transform()
+        self._program.vert['transform'] = tr.shader_map()
         self._program.prepare()  # Force ModularProgram to set shaders
         self._program['a_position'] = gloo.VertexBuffer(self._pos)
         self._program['a_color'] = gloo.VertexBuffer(self._color)
         self._program['a_size'] = gloo.VertexBuffer(self._size)
         self._program.draw('points')
-    
+
 
 class Canvas(app.Canvas):
 
     def __init__(self):
         app.Canvas.__init__(self, keys='interactive')
+        ps = self.pixel_scale
 
         n = 10000
         pos = 0.25 * np.random.randn(n, 2).astype(np.float32)
         color = np.random.uniform(0, 1, (n, 3)).astype(np.float32)
-        size = np.random.uniform(2, 12, (n, 1)).astype(np.float32)
+        size = np.random.uniform(2*ps, 12*ps, (n, 1)).astype(np.float32)
 
         self.points = MarkerVisual(pos=pos, color=color, size=size)
-        
-        # This is just an instance I choose to use, nothing required in the
-        # Visual API here.
-        self.panzoom = PanZoomTransform()
-        
-        # Here, I set the `transform` hook to my PAN_ZOOM function.
-        # In addition, I provide a component instance.
-        # Vispy knows that every $variable in the Function is bound to
-        # component.$variable. Here, since pan and zoom are tuples,
-        # Vispy understands that it has to create two uniforms (u_$variable 
-        # for example).
-        tr = self.panzoom * LogTransform(base=(0, 2, 0))
-        self.points._program.vert['transform'] = tr.shader_map()
-    
-    def on_initialize(self, even):
+
+        self.panzoom = STTransform(scale=(1, 0.2), translate=(0, 500))
+        w2 = (self.size[0]/2, self.size[1]/2)
+        self.transform = ChainTransform([self.panzoom,
+                                         STTransform(scale=w2, translate=w2),
+                                         LogTransform(base=(0, 2, 0))])
+
+        self.tr_sys = TransformSystem(self)
+        self.tr_sys.visual_to_document = self.transform
+
         gloo.set_state(blend=True,
                        blend_func=('src_alpha', 'one_minus_src_alpha'))
-    
-    def _normalize(self, xy):
-        x, y = xy
-        w, h = float(self.width), float(self.height)
-        return x/(w/2.)-1., y/(h/2.)-1.
-        
+
     def on_mouse_move(self, event):
         if event.is_dragging:
-            x0, y0 = event.press_event.pos
-            x1, y1 = event.last_event.pos
-            x, y = event.pos
-            dxy = ((x - x1) / self.size[0] * 2, -(y - y1) / self.size[1] * 2)
+            dxy = event.pos - event.last_event.pos
             button = event.press_event.button
-            
-            # This just updates my private PanZoom instance. Nothing magic
-            # happens.
+
             if button == 1:
                 self.panzoom.move(dxy)
             elif button == 2:
-                self.panzoom.zoom(dxy)
-                
-            # The magic happens here. self.on_draw() is called, so 
-            # self.points.draw() is called. The two variables in the transform
-            # hook are bound to self.panzoom.pan and self.panzoom.zoom, so
-            # Vispy will automatically fetch those two values and update
-            # the two corresponding uniforms.
+                center = event.press_event.pos
+                self.panzoom.zoom(np.exp(dxy * (0.01, -0.01)), center)
+
             self.update()
-            
-            # Force transform to update its shader. 
-            # (this should not be necessary)
-            self.panzoom.shader_map()
-        
+
     def on_resize(self, event):
         self.width, self.height = event.size
         gloo.set_viewport(0, 0, self.width, self.height)
 
     def on_draw(self, event):
         gloo.clear()
-        self.points.draw()
+        self.points.draw(self.tr_sys)
 
 if __name__ == '__main__':
     c = Canvas()
diff --git a/examples/basics/visuals/dynamic_polygon.py b/examples/basics/visuals/dynamic_polygon.py
new file mode 100644
index 0000000..021a5cf
--- /dev/null
+++ b/examples/basics/visuals/dynamic_polygon.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+Demonstration of Polygon and subclasses
+"""
+
+import sys
+import numpy as np
+
+from vispy import app, gloo, visuals
+from vispy.visuals import transforms
+
+# vertex positions of polygon data to draw
+pos = np.array([[0, 0, 0],
+               [0.25, 0.22, 0],
+               [0.25, 0.5, 0],
+               [0, 0.5, 0],
+               [-0.25, 0.25, 0]])
+
+pos = np.array([[0, 0],
+                [10, 0],
+                [10, 10],
+                [20, 10],
+                [20, 20],
+                [25, 20],
+                [25, 25],
+                [20, 25],
+                [20, 20],
+                [10, 17],
+                [5, 25],
+                [9, 30],
+                [6, 15],
+                [15, 12.5],
+                [0, 5]])
+
+theta = np.linspace(0, 2*np.pi, 11)
+pos = np.hstack([np.cos(theta)[:, np.newaxis],
+                 np.sin(theta)[:, np.newaxis]])
+pos[::2] *= 0.4
+pos[-1] = pos[0]
+
+
+class Canvas(app.Canvas):
+    def __init__(self):
+        app.Canvas.__init__(self, keys='interactive', size=(800, 800))
+        global pos
+        self.visuals = []
+        polygon = visuals.PolygonVisual(pos=pos, color=(0.8, .2, 0, 1),
+                                        border_color=(1, 1, 1, 1))
+        polygon.transform = transforms.STTransform(scale=(200, 200),
+                                                   translate=(600, 600))
+        self.visuals.append(polygon)
+
+        ellipse = visuals.EllipseVisual(pos=(0, 0, 0), radius=(100, 100),
+                                        color=(0.2, 0.2, 0.8, 1),
+                                        border_color=(1, 1, 1, 1),
+                                        start_angle=180., span_angle=150.)
+        ellipse.transform = transforms.STTransform(scale=(0.9, 1.5),
+                                                   translate=(200, 200))
+        self.visuals.append(ellipse)
+
+        rect = visuals.RectangleVisual(pos=(600, 200, 0), height=200.,
+                                       width=300.,
+                                       radius=[30., 30., 0., 0.],
+                                       color=(0.5, 0.5, 0.2, 1),
+                                       border_color='white')
+        rect.transform = transforms.NullTransform()
+        self.visuals.append(rect)
+
+        rpolygon = visuals.RegularPolygonVisual(pos=(200., 600., 0), 
+                                                radius=160,
+                                                color=(0.2, 0.8, 0.2, 1),
+                                                border_color=(1, 1, 1, 1),
+                                                sides=6)
+        rpolygon.transform = transforms.NullTransform()
+        self.visuals.append(rpolygon)
+
+        for v in self.visuals:
+            v.tr_sys = transforms.TransformSystem(self)
+            v.tr_sys.visual_to_document = v.transform
+
+        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
+
+        self.show()
+
+    def on_draw(self, ev):
+        gloo.set_clear_color((0, 0, 0, 1))
+        gloo.set_viewport(0, 0, *self.physical_size)
+        gloo.clear()
+        for vis in self.visuals:
+            vis.draw(vis.tr_sys)
+
+    def on_timer(self, event):
+        polygon, ellipse, rect, rpolygon = self.visuals
+        r = ellipse.radius
+        ellipse.radius = r[0], r[1] + np.sin(event.elapsed * 10)
+        ellipse.span_angle = (ellipse.span_angle + 100. * event.dt) % 360
+        
+        polygon.color = (0.3 * (0.5 + np.sin(event.elapsed*2 + 0)),
+                         0.3 * (0.5 + np.sin(event.elapsed*2 + np.pi * 2./3.)),
+                         0.3 * (0.5 + np.sin(event.elapsed*2 + np.pi * 4./3.)),
+                         )
+        polygon.border_color = (.8, .8, .8, 
+                                0.5 + (0.5 * np.sin(event.elapsed*10)))
+        
+        rpolygon.radius = 100 + 10 * np.sin(event.elapsed * 3.1)
+        rpolygon.sides = int(20 + 17 * np.sin(event.elapsed))
+        
+        self.update()
+
+
+if __name__ == '__main__':
+    win = Canvas() 
+    if sys.flags.interactive != 1:
+        win.app.run()
diff --git a/examples/basics/visuals/image_transforms.py b/examples/basics/visuals/image_transforms.py
index c187580..b9573a5 100644
--- a/examples/basics/visuals/image_transforms.py
+++ b/examples/basics/visuals/image_transforms.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # vispy: gallery 2
 
@@ -10,10 +10,11 @@ Simple demonstration of ImageVisual.
 import numpy as np
 import vispy.app
 from vispy import gloo
-from vispy.scene import visuals
-from vispy.scene.transforms import (AffineTransform, STTransform, arg_to_array,
-                                    LogTransform, PolarTransform, 
-                                    BaseTransform)
+from vispy import visuals
+from vispy.visuals.transforms import (AffineTransform, STTransform,
+                                      arg_to_array, TransformSystem,
+                                      LogTransform, PolarTransform,
+                                      BaseTransform)
 
 image = np.random.normal(size=(100, 100, 3))
 image[20:80, 20:80] += 3.
@@ -24,18 +25,21 @@ image = ((image-image.min()) *
          (253. / (image.max()-image.min()))).astype(np.ubyte)
 
 
-class Canvas(vispy.scene.SceneCanvas):
+class Canvas(vispy.app.Canvas):
     def __init__(self):
-        self.images = [visuals.Image(image, method='impostor')
+        vispy.app.Canvas.__init__(self, keys='interactive', size=(800, 800))
+
+        self.images = [visuals.ImageVisual(image, method='impostor')
                        for i in range(4)]
         self.images[0].transform = (STTransform(scale=(30, 30),
-                                                translate=(600, 600)) * 
+                                                translate=(600, 600)) *
                                     SineTransform() *
                                     STTransform(scale=(0.1, 0.1),
                                                 translate=(-5, -5)))
 
         tr = AffineTransform()
         tr.rotate(30, (0, 0, 1))
+        tr.rotate(40, (0, 1, 0))
         tr.scale((3, 3))
         self.images[1].transform = (STTransform(translate=(200, 600)) *
                                     tr *
@@ -53,15 +57,18 @@ class Canvas(vispy.scene.SceneCanvas):
                                     STTransform(scale=(np.pi/200, 0.005),
                                                 translate=(-3*np.pi/4., 0.1)))
 
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.size = (800, 800)
+        for img in self.images:
+            img.tr_sys = TransformSystem(self)
+            img.tr_sys.visual_to_document = img.transform
+
         self.show()
 
     def on_draw(self, ev):
         gloo.clear(color='black', depth=True)
-        self.push_viewport((0, 0) + self.size)
+        gloo.set_viewport(0, 0, *self.physical_size)
+        # Create a TransformSystem that will tell the visual how to draw
         for img in self.images:
-            self.draw_visual(img)
+            img.draw(img.tr_sys)
 
 
 # A simple custom Transform
diff --git a/examples/basics/visuals/image_visual.py b/examples/basics/visuals/image_visual.py
index e3f2afa..3b21cfb 100644
--- a/examples/basics/visuals/image_visual.py
+++ b/examples/basics/visuals/image_visual.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -9,25 +9,30 @@ Simple demonstration of ImageVisual.
 import numpy as np
 import vispy.app
 from vispy import gloo
-from vispy.scene import visuals
-from vispy.scene.transforms import STTransform
+from vispy import visuals
+from vispy.visuals.transforms import STTransform, TransformSystem
 
 image = np.random.normal(size=(100, 100, 3), loc=128,
                          scale=50).astype(np.ubyte)
 
 
-class Canvas(vispy.scene.SceneCanvas):
+class Canvas(vispy.app.Canvas):
     def __init__(self):
-        self.image = visuals.Image(image, method='subdivide')
-        self.image.transform = STTransform(scale=(7, 7), translate=(50, 50))
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.size = (800, 800)
+        vispy.app.Canvas.__init__(self, keys='interactive', size=(800, 800))
+
+        self.image = visuals.ImageVisual(image, method='subdivide')
+        self.image_transform = STTransform(scale=(7, 7), translate=(50, 50))
+
+        # Create a TransformSystem that will tell the visual how to draw
+        self.tr_sys = TransformSystem(self)
+        self.tr_sys.visual_to_document = self.image_transform
+
         self.show()
 
     def on_draw(self, ev):
         gloo.clear(color='black', depth=True)
-        self.push_viewport((0, 0) + self.size)
-        self.draw_visual(self.image)
+        gloo.set_viewport(0, 0, *self.physical_size)
+        self.image.draw(self.tr_sys)
 
 
 if __name__ == '__main__':
diff --git a/examples/basics/visuals/line.py b/examples/basics/visuals/line.py
index c822d2f..cda3c86 100644
--- a/examples/basics/visuals/line.py
+++ b/examples/basics/visuals/line.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
@@ -10,9 +10,8 @@ Demonstration of various features of Line visual.
 import sys
 import numpy as np
 
-import vispy.app
-from vispy.scene import visuals
-from vispy.scene.transforms import STTransform
+from vispy import app, gloo, visuals
+from vispy.visuals.transforms import STTransform, NullTransform
 
 # vertex positions of data to draw
 N = 200
@@ -32,41 +31,65 @@ connect[:, 1] = connect[:, 0] + 1
 connect[N/2, 1] = N/2  # put a break in the middle
 
 
-class Canvas(vispy.scene.SceneCanvas):
+class Canvas(app.Canvas):
     def __init__(self):
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive',
-                                         size=(800, 800), show=True)
+        app.Canvas.__init__(self, keys='interactive',
+                            size=(800, 800))
         # Create several visuals demonstrating different features of Line
         self.lines = [
-            # agg-mode lines:
-            visuals.Line(pos=pos, color=color, mode='agg'),  # per-vertex color
-            visuals.Line(pos=pos, color=(0, 0.5, 0.3, 1), mode='agg'),  # solid
-            visuals.Line(pos=pos, color=color, width=5, mode='agg'),  # wide
-            # GL-mode lines:
-            visuals.Line(pos=pos, color=color, mode='gl'),
-            visuals.Line(pos=pos, color=(0, 0.5, 0.3, 1), mode='gl'),
-            visuals.Line(pos=pos, color=color, width=5, mode='gl'),
-            # GL-mode: "connect" not available in AGG mode yet
-            visuals.Line(pos=pos, color=(0, 0.5, 0.3, 1), connect='segments',
-                         mode='gl'),  # only connect alternate vert pairs
-            visuals.Line(pos=pos, color=(0, 0.5, 0.3, 1), connect=connect,
-                         mode='gl'),  # connect specific pairs
+            # agg-method lines:
+            # per-vertex color
+            visuals.LineVisual(pos=pos, color=color, method='agg'),
+            # solid
+            visuals.LineVisual(pos=pos, color=(0, 0.5, 0.3, 1), method='agg'),
+            # wide
+            visuals.LineVisual(pos=pos, color=color, width=5, method='agg'),
+
+            # GL-method lines:
+            visuals.LineVisual(pos=pos, color=color, method='gl'),
+            visuals.LineVisual(pos=pos, color=(0, 0.5, 0.3, 1), method='gl'),
+            visuals.LineVisual(pos=pos, color=color, width=5, method='gl'),
+            # GL-method: "connect" not available in AGG method yet
+
+            # only connect alternate vert pairs
+            visuals.LineVisual(pos=pos, color=(0, 0.5, 0.3, 1),
+                               connect='segments', method='gl'),
+            # connect specific pairs
+            visuals.LineVisual(pos=pos, color=(0, 0.5, 0.3, 1),
+                               connect=connect, method='gl'),
         ]
         counts = [0, 0]
         for i, line in enumerate(self.lines):
             # arrange lines in a grid
-            tidx = (line.mode == 'agg')
+            tidx = (line.method == 'agg')
             x = 400 * tidx
             y = 140 * (counts[tidx] + 1)
             counts[tidx] += 1
             line.transform = STTransform(translate=[x, y])
             # redraw the canvas if any visuals request an update
             line.events.update.connect(lambda evt: self.update())
-            line.parent = self.central_widget
-        self.texts = [visuals.Text('GL', bold=True, font_size=24, color='w',
-                                   pos=(200, 40), parent=self.central_widget),
-                      visuals.Text('Agg', bold=True, font_size=24, color='w',
-                                   pos=(600, 40), parent=self.central_widget)]
+
+        self.texts = [visuals.TextVisual('GL', bold=True, font_size=24,
+                                         color='w', pos=(200, 40)),
+                      visuals.TextVisual('Agg', bold=True, font_size=24,
+                                         color='w', pos=(600, 40))]
+        for text in self.texts:
+            text.transform = NullTransform()
+        self.visuals = self.lines + self.texts
+
+        # create a TransformSystem for each visual.
+        # (these are stored as attributes of each visual for convenience)
+        for visual in self.visuals:
+            visual.tr_sys = visuals.transforms.TransformSystem(self)
+            visual.tr_sys.visual_to_document = visual.transform
+
+        self.show()
+
+    def on_draw(self, event):
+        gloo.clear('black')
+        gloo.set_viewport(0, 0, *self.physical_size)
+        for visual in self.visuals:
+            visual.draw(visual.tr_sys)
 
 
 if __name__ == '__main__':
@@ -77,9 +100,9 @@ if __name__ == '__main__':
         win.lines[0].set_data(pos)
         win.lines[3].set_data(pos)
 
-    timer = vispy.app.Timer()
+    timer = app.Timer()
     timer.connect(update)
     timer.start(0)
 
     if sys.flags.interactive != 1:
-        vispy.app.run()
+        app.run()
diff --git a/examples/basics/visuals/line_plot.py b/examples/basics/visuals/line_plot.py
index 91dff9e..05f6369 100644
--- a/examples/basics/visuals/line_plot.py
+++ b/examples/basics/visuals/line_plot.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# vispy: testskip (KNOWNFAIL)
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -7,8 +8,9 @@ Simple demonstration of LinePlot visual.
 """
 
 import numpy as np
-import vispy.app
-from vispy.scene import visuals
+import sys
+
+from vispy import gloo, app, visuals
 
 # vertex positions of data to draw
 N = 20
@@ -17,17 +19,25 @@ pos[:, 0] = np.linspace(10, 790, N)
 pos[:, 1] = np.random.normal(size=N, scale=100, loc=400)
 
 
-class Canvas(vispy.scene.SceneCanvas):
+class Canvas(app.Canvas):
     def __init__(self):
-        self.line = visuals.LinePlot(pos, color='w', edge_color='w',
-                                     face_color=(0.2, 0.2, 1))
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive',
-                                         size=(800, 800), show=True)
-        self.line.parent = self.scene
+        app.Canvas.__init__(self, keys='interactive',
+                            size=(800, 800))
+
+        self.line = visuals.LinePlotVisual(pos, color='w', edge_color='w',
+                                           face_color=(0.2, 0.2, 1))
+
+        self.tr_sys = visuals.transforms.TransformSystem(self)
+
+        self.show()
+
+    def on_draw(self, event):
+        gloo.clear('black')
+        gloo.set_viewport(0, 0, *self.physical_size)
+        self.line.draw(self.tr_sys)
 
 
 if __name__ == '__main__':
     win = Canvas()
-    import sys
     if sys.flags.interactive != 1:
-        vispy.app.run()
+        app.run()
diff --git a/examples/basics/visuals/line_transform.py b/examples/basics/visuals/line_transform.py
index 90fbe43..a80daf2 100644
--- a/examples/basics/visuals/line_transform.py
+++ b/examples/basics/visuals/line_transform.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 1
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -11,12 +11,9 @@ information, but different transformations.
 """
 
 import numpy as np
-import vispy.app
-from vispy import gloo
-from vispy.scene import visuals
-from vispy.scene.transforms import (STTransform, LogTransform,
-                                    AffineTransform, PolarTransform)
-
+from vispy import app, gloo, visuals
+from vispy.visuals.transforms import (STTransform, LogTransform,
+                                      AffineTransform, PolarTransform)
 import vispy.util
 vispy.util.use_log_level('debug')
 
@@ -32,29 +29,30 @@ color[:, 0] = np.linspace(0, 1, N)
 color[:, 1] = color[::-1, 0]
 
 
-class Canvas(vispy.scene.SceneCanvas):
+class Canvas(app.Canvas):
     def __init__(self):
+        app.Canvas.__init__(self, keys='interactive', size=(800, 800))
 
         # Define several Line visuals that use the same position data
         # but have different colors and transformations
         colors = [color, (1, 0, 0, 1), (0, 1, 0, 1), (0, 0, 1, 1),
                   (1, 1, 0, 1), (1, 1, 1, 1)]
 
-        self.lines = [visuals.Line(pos=pos, color=colors[i])
+        self.lines = [visuals.LineVisual(pos=pos, color=colors[i])
                       for i in range(6)]
 
         center = STTransform(translate=(400, 400))
 
         self.lines[0].transform = center
 
-        self.lines[1].transform = (center * 
+        self.lines[1].transform = (center *
                                    STTransform(scale=(1, 0.1, 1)))
 
-        self.lines[2].transform = (center * 
+        self.lines[2].transform = (center *
                                    STTransform(translate=(200, 200, 0)) *
                                    STTransform(scale=(0.3, 0.5, 1)))
 
-        self.lines[3].transform = (center * 
+        self.lines[3].transform = (center *
                                    STTransform(translate=(-200, -200, 0),
                                                scale=(200, 1)) *
                                    LogTransform(base=(10, 0, 0)) *
@@ -72,20 +70,21 @@ class Canvas(vispy.scene.SceneCanvas):
                                    STTransform(scale=(0.01, 0.1),
                                                translate=(4, 20)))
 
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.size = (800, 800)
+        for line in self.lines:
+            tr_sys = visuals.transforms.TransformSystem(self)
+            tr_sys.visual_to_document = line.transform
+            line.tr_sys = tr_sys
+
         self.show()
 
     def on_draw(self, ev):
-        gloo.set_clear_color('black')
-        gloo.clear(color=True, depth=True)
-        gloo.set_viewport(0, 0, *self.size)
+        gloo.clear('black', depth=True)
+        gloo.set_viewport(0, 0, *self.physical_size)
         for line in self.lines:
-            self.draw_visual(line)
-
+            line.draw(line.tr_sys)
 
 if __name__ == '__main__':
     win = Canvas()
     import sys
     if sys.flags.interactive != 1:
-        vispy.app.run()
+        app.run()
diff --git a/examples/basics/visuals/line_update.py b/examples/basics/visuals/line_update.py
deleted file mode 100644
index 6112561..0000000
--- a/examples/basics/visuals/line_update.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-"""
-Demonstration of animated Line visual.
-"""
-
-import numpy as np
-import vispy.app
-from vispy.scene import visuals
-
-# vertex positions of data to draw
-N = 200
-pos = np.zeros((N, 2), dtype=np.float32)
-pos[:, 0] = np.linspace(50., 750., N)
-pos[:, 1] = np.random.normal(size=N, scale=100, loc=400)
-
-# color array
-color = np.ones((N, 4), dtype=np.float32)
-color[:, 0] = np.linspace(0, 1, N)
-color[:, 1] = color[::-1, 0]
-
-
-class Canvas(vispy.scene.SceneCanvas):
-    def __init__(self):
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive',
-                                         size=(800, 800), show=True)
-        self.line = visuals.Line(pos, color, parent=self.scene)
-        self.line.events.update.connect(lambda evt: self.update)
-
-
-if __name__ == '__main__':
-    win = Canvas()
-
-    def update(ev):
-        pos[:, 1] = np.random.normal(size=N, scale=100, loc=400)
-        win.line.set_data(pos=pos)
-
-    timer = vispy.app.Timer()
-    timer.connect(update)
-    timer.start(0)
-
-    import sys
-    if sys.flags.interactive != 1:
-        vispy.app.run()
diff --git a/examples/basics/visuals/markers.py b/examples/basics/visuals/markers.py
index e67c862..99b24fc 100644
--- a/examples/basics/visuals/markers.py
+++ b/examples/basics/visuals/markers.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """ Display markers at different sizes and line thicknessess.
@@ -9,42 +9,41 @@
 
 import numpy as np
 
-from vispy import app, gloo
-from vispy.scene.visuals import Markers, marker_types
-from vispy.scene.transforms import STTransform
+from vispy import app, gloo, visuals
+from vispy.visuals.transforms import STTransform, TransformSystem
 
-n = 540
+n = 500
 pos = np.zeros((n, 2))
+colors = np.ones((n, 4), dtype=np.float32)
 radius, theta, dtheta = 1.0, 0.0, 5.5 / 180.0 * np.pi
 for i in range(500):
     theta += dtheta
     x = 256 + radius * np.cos(theta)
-    y = 256 + 32 + radius * np.sin(theta)
+    y = 256 + radius * np.sin(theta)
     r = 10.1 - i * 0.02
     radius -= 0.45
     pos[i] = x, y
+    colors[i] = (i/500, 1.0-i/500, 0, 1)
 
 
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive', size=(512, 512 + 2*32),
+        app.Canvas.__init__(self, keys='interactive', size=(512, 512),
                             title="Marker demo [press space to change marker]")
         self.index = 0
         self.scale = 1.
-        self.markers = Markers()
-        self.markers.set_data(pos)
-        self.markers.set_style(marker_types[self.index])
+        self.tr_sys = TransformSystem(self)
+        self.tr_sys.visual_to_document = STTransform()
+        self.markers = visuals.MarkersVisual()
+        self.markers.set_data(pos, face_color=colors)
+        self.markers.set_symbol(visuals.marker_types[self.index])
 
-    def on_initialize(self, event):
-        # We need to give a transform to our visual
-        self.transform = STTransform()
-        self.markers._program.vert['transform'] = self.transform.shader_map()
-        self.apply_zoom()
+        self.show()
 
     def on_draw(self, event):
         gloo.clear(color='white')
-        self.markers.draw()
+        self.markers.draw(self.tr_sys)
 
     def on_mouse_wheel(self, event):
         """Use the mouse wheel to zoom."""
@@ -56,19 +55,16 @@ class Canvas(app.Canvas):
         self.apply_zoom()
 
     def apply_zoom(self):
-        gloo.set_viewport(0, 0, *self.size)
-        self.transform.scale = (2 * self.scale / self.size[0],
-                                2 * self.scale / self.size[1], 1.)
-        self.transform.translate = [-1, -1]
+        gloo.set_viewport(0, 0, *self.physical_size)
+        self.tr_sys.visual_to_document.scale = (self.scale, self.scale)
         self.update()
 
     def on_key_press(self, event):
         if event.text == ' ':
-            self.index = (self.index + 1) % (len(marker_types))
-            self.markers.set_style(marker_types[self.index])
+            self.index = (self.index + 1) % (len(visuals.marker_types))
+            self.markers.set_symbol(visuals.marker_types[self.index])
             self.update()
 
 if __name__ == '__main__':
     canvas = Canvas()
-    canvas.show()
     app.run()
diff --git a/examples/basics/visuals/mesh.py b/examples/basics/visuals/mesh.py
index dcf9efa..0939337 100644
--- a/examples/basics/visuals/mesh.py
+++ b/examples/basics/visuals/mesh.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -7,16 +7,16 @@ Simple demonstration of Mesh visual.
 """
 
 import numpy as np
-import vispy.app
-from vispy import gloo
-from vispy.scene.visuals import Mesh
+from vispy import app, gloo, visuals
 from vispy.geometry import create_sphere
-from vispy.scene.transforms import (STTransform, AffineTransform,
-                                    ChainTransform)
+from vispy.visuals.transforms import (STTransform, AffineTransform,
+                                      ChainTransform)
 
 
-class Canvas(vispy.scene.SceneCanvas):
+class Canvas(app.Canvas):
     def __init__(self):
+        app.Canvas.__init__(self, keys='interactive', size=(800, 550))
+        
         self.meshes = []
         self.rotation = AffineTransform()
 
@@ -25,36 +25,37 @@ class Canvas(vispy.scene.SceneCanvas):
         mdata = create_sphere(20, 40, 1.0)
 
         # Mesh with pre-indexed vertices, uniform color
-        self.meshes.append(Mesh(meshdata=mdata, color='r'))
-        #mesh.transform = STTransform(scale=(1, 1, .001), translate=(400, 400))
+        self.meshes.append(visuals.MeshVisual(meshdata=mdata, color='r'))
 
         ## Mesh with pre-indexed vertices, per-face color
         ##   Because vertices are pre-indexed, we get a different color
         ##   every time a vertex is visited, resulting in sharp color
         ##   differences between edges.
-        verts = mdata.vertices(indexed='faces')
+        verts = mdata.get_vertices(indexed='faces')
         nf = verts.size//9
         fcolor = np.ones((nf, 3, 4), dtype=np.float32)
         fcolor[..., 0] = np.linspace(1, 0, nf)[:, np.newaxis]
         fcolor[..., 1] = np.random.normal(size=nf)[:, np.newaxis]
         fcolor[..., 2] = np.linspace(0, 1, nf)[:, np.newaxis]
-        mesh = Mesh(vertices=verts, face_colors=fcolor)
+        mesh = visuals.MeshVisual(vertices=verts, face_colors=fcolor)
         self.meshes.append(mesh)
 
         ## Mesh with unindexed vertices, per-vertex color
         ##   Because vertices are unindexed, we get the same color
         ##   every time a vertex is visited, resulting in no color differences
         ##   between edges.
-        verts = mdata.vertices()
-        faces = mdata.faces()
+        verts = mdata.get_vertices()
+        faces = mdata.get_faces()
         nv = verts.size//3
         vcolor = np.ones((nv, 4), dtype=np.float32)
         vcolor[:, 0] = np.linspace(1, 0, nv)
         vcolor[:, 1] = np.random.normal(size=nv)
         vcolor[:, 2] = np.linspace(0, 1, nv)
-        self.meshes.append(Mesh(verts, faces, vcolor))
-        self.meshes.append(Mesh(verts, faces, vcolor, shading='flat'))
-        self.meshes.append(Mesh(verts, faces, vcolor, shading='smooth'))
+        self.meshes.append(visuals.MeshVisual(verts, faces, vcolor))
+        self.meshes.append(visuals.MeshVisual(verts, faces, vcolor, 
+                                              shading='flat'))
+        self.meshes.append(visuals.MeshVisual(verts, faces, vcolor, 
+                                              shading='smooth'))
 
         # Lay out meshes in a grid
         grid = (3, 3)
@@ -62,34 +63,31 @@ class Canvas(vispy.scene.SceneCanvas):
         for i, mesh in enumerate(self.meshes):
             x = 800. * (i % grid[0]) / grid[0] + 400. / grid[0] - 2
             y = 800. * (i // grid[1]) / grid[1] + 400. / grid[1] + 2
-            mesh.transform = ChainTransform([STTransform(translate=(x, y),
-                                                         scale=(s, s, 1)),
-                                             self.rotation])
+            transform = ChainTransform([STTransform(translate=(x, y),
+                                                    scale=(s, s, 1)),
+                                        self.rotation])
+            tr_sys = visuals.transforms.TransformSystem(self)
+            tr_sys.visual_to_document = transform
+            mesh.tr_sys = tr_sys
 
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-
-        self.size = (800, 800)
         self.show()
 
-        self.timer = vispy.app.Timer(connect=self.rotate)
+        self.timer = app.Timer(connect=self.rotate)
         self.timer.start(0.016)
 
     def rotate(self, event):
         self.rotation.rotate(1, (0, 1, 0))
-        # TODO: altering rotation should trigger this automatically.
-        for m in self.meshes:
-            m._program._need_build = True
         self.update()
 
     def on_draw(self, ev):
-        gloo.set_clear_color('black')
-        gloo.clear(color=True, depth=True)
+        gloo.set_viewport(0, 0, *self.physical_size)
+        gloo.clear(color='black', depth=True)
         for mesh in self.meshes:
-            self.draw_visual(mesh)
+            mesh.draw(mesh.tr_sys)
 
 
 if __name__ == '__main__':
     win = Canvas()
     import sys
     if sys.flags.interactive != 1:
-        vispy.app.run()
+        app.run()
diff --git a/examples/basics/visuals/modular_components.py b/examples/basics/visuals/modular_components.py
deleted file mode 100644
index 438a154..0000000
--- a/examples/basics/visuals/modular_components.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-# -----------------------------------------------------------------------------
-"""
-Demonstrates plugging custom shaders in to a ModularLine visual.
-
-This allows to modify the appearance of the visual without modifying or
-subclassing the original ModularLine class.
-"""
-
-import numpy as np
-import vispy.app
-import vispy.gloo as gloo
-from vispy.scene.visuals.modular_line import ModularLine
-from vispy.scene.transforms import BaseTransform, STTransform, arg_to_array
-from vispy.scene.components import (VisualComponent, VertexColorComponent,
-                                    XYPosComponent)
-from vispy.scene.shaders import Varying
-
-# vertex positions of data to draw
-N = 50
-pos = np.zeros((N, 2), dtype=np.float32)
-pos[:, 0] = np.linspace(-0.9, 0.9, N)
-pos[:, 1] = np.random.normal(size=N, scale=0.2).astype(np.float32)
-
-# One array of colors
-color = np.ones((N, 4), dtype=np.float32)
-color[:, 0] = np.linspace(0, 1, N)
-color[:, 1] = color[::-1, 0]
-
-
-# A custom Transform
-class SineTransform(BaseTransform):
-    """
-    Add sine wave to y-value for wavy effect.
-    """
-    glsl_map = """
-        vec4 sineTransform(vec4 pos) {
-            return vec4(pos.x, pos.y + sin(pos.x), pos.z, 1);
-        }"""
-
-    @arg_to_array
-    def map(self, coords):
-        ret = coords.copy()
-        ret[..., 1] += np.sin(ret[..., 0])
-        return ret
-
-    @arg_to_array
-    def imap(self, coords):
-        ret = coords.copy()
-        ret[..., 1] -= np.sin(ret[..., 0])
-        return ret
-
-
-# Custom color component
-class DashComponent(VisualComponent):
-    """
-    VisualComponent that adds dashing to an attached LineVisual.
-    """
-
-    SHADERS = dict(
-        frag_color="""
-            vec4 dash(vec4 color) {
-                float mod = $distance / $dash_len;
-                mod = mod - float(int(mod));
-                color.a = 0.5 * sin(mod*3.141593*2.) + 0.5;
-                return color;
-            }
-        """,
-        vert_post_hook="""
-            void dashSup() {
-                $output_dist = $distance_attr;
-            }
-        """)
-
-    def __init__(self, pos):
-        super(DashComponent, self).__init__()
-        self._vbo = None
-        self.pos = pos
-
-    def _make_vbo(self):
-        if self._vbo is None:
-            # measure distance along line
-            # TODO: this should be recomputed if the line data changes.
-            pixel_tr = self.visual.transform
-            pixel_pos = pixel_tr.map(self.pos)
-            dist = np.empty(pos.shape[0], dtype=np.float32)
-            diff = ((pixel_pos[1:] - pixel_pos[:-1]) ** 2).sum(axis=1) ** 0.5
-            dist[0] = 0.0
-            dist[1:] = np.cumsum(diff)
-            self._vbo = gloo.VertexBuffer(dist)
-        return self._vbo
-
-    def activate(self, program, mode):
-        vf = self._funcs['vert_post_hook']
-        ff = self._funcs['frag_color']
-        vf['distance_attr'] = self._make_vbo()  # attribute float
-        vf['output_dist'] = Varying('output_dist', dtype='float')
-        ff['dash_len'] = 20.
-        ff['distance'] = vf['output_dist']
-
-    @property
-    def supported_draw_modes(self):
-        return set((self.DRAW_PRE_INDEXED,))
-
-
-# custom position component
-class WobbleComponent(VisualComponent):
-    """
-    Give all vertices a wobble with random phase.
-    """
-    SHADERS = dict(
-        local_position="""
-            vec4 wobble(vec4 pos) {
-                float x = pos.x + 0.01 * cos($theta + $phase);
-                float y = pos.y + 0.01 * sin($theta + $phase);
-                return vec4(x, y, pos.z, pos.w);
-            }
-        """)
-
-    def __init__(self, pos):
-        super(WobbleComponent, self).__init__()
-        self._vbo = None
-        self.pos = pos
-        self.theta = (np.random.random(size=pos.shape[:-1]).astype(np.float32)
-                      * (2. * np.pi))
-        self.phase = 0
-
-    def activate(self, program, mode):
-        if self._vbo is None:
-            self._vbo = gloo.VertexBuffer(self.theta)
-
-        pf = self._funcs['local_position']
-        pf['theta'] = self._vbo
-        pf['phase'] = self.phase
-
-        # TODO: make this automatic
-        self._visual._program._need_build = True
-
-
-class Canvas(vispy.scene.SceneCanvas):
-    def __init__(self):
-
-        self.line = ModularLine()
-        self.line.transform = (STTransform(scale=(40, 100), 
-                                           translate=(400, 400)) *
-                               SineTransform() *
-                               STTransform(scale=(10, 3)))
-        self.wobbler = WobbleComponent(pos)
-        self.line.pos_components = [XYPosComponent(pos), self.wobbler]
-        dasher = DashComponent(pos)
-        self.line.color_components = [VertexColorComponent(color), dasher]
-
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.size = (800, 800)
-        self.show()
-
-        self.timer = vispy.app.Timer(connect=self.wobble,
-                                     interval=0.02,
-                                     start=True)
-
-    def on_draw(self, ev):
-        gloo.set_clear_color('black')
-        gloo.clear(color=True, depth=True)
-
-        self.draw_visual(self.line)
-
-    def wobble(self, ev):
-        self.wobbler.phase += 0.1
-        self.update()
-
-
-if __name__ == '__main__':
-    win = Canvas()
-    import sys
-    if sys.flags.interactive != 1:
-        vispy.app.run()
diff --git a/examples/basics/visuals/modular_line.py b/examples/basics/visuals/modular_line.py
deleted file mode 100644
index ae48dd7..0000000
--- a/examples/basics/visuals/modular_line.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-"""
-Simple demonstration of LineVisual.
-"""
-
-import numpy as np
-import vispy.app
-from vispy import gloo
-from vispy.scene.visuals.modular_line import ModularLine
-
-# vertex positions of data to draw
-N = 200
-pos = np.zeros((N, 3), dtype=np.float32)
-pos[:, 0] = np.linspace(100, 700, N)
-pos[:, 1] = np.random.normal(size=N, scale=100, loc=400)
-
-# color array
-color = np.ones((N, 4), dtype=np.float32)
-color[:, 0] = np.linspace(0, 1, N)
-color[:, 1] = color[::-1, 0]
-
-
-class Canvas(vispy.scene.SceneCanvas):
-    def __init__(self):
-        self.line = ModularLine(pos=pos, color=color)
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.size = (800, 800)
-        self.show()
-
-    def on_draw(self, ev):
-        gloo.set_clear_color('black')
-        gloo.clear(color=True, depth=True)
-        gloo.set_viewport(0, 0, *self.size)
-        self.draw_visual(self.line)
-
-
-if __name__ == '__main__':
-    win = Canvas()
-    import sys
-    if sys.flags.interactive != 1:
-        vispy.app.run()
diff --git a/examples/basics/visuals/modular_mesh.py b/examples/basics/visuals/modular_mesh.py
deleted file mode 100644
index c6fd163..0000000
--- a/examples/basics/visuals/modular_mesh.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-# vispy: gallery 30
-
-"""
-Simple demonstration of LineVisual.
-"""
-
-import numpy as np
-import vispy.app
-from vispy import gloo
-from vispy.scene.visuals.modular_mesh import ModularMesh
-from vispy.scene.components import (VertexColorComponent, GridContourComponent,
-                                    VertexNormalComponent, ShadingComponent)
-from vispy.geometry import create_sphere
-from vispy.scene.transforms import (STTransform, AffineTransform,
-                                    ChainTransform)
-
-
-class Canvas(vispy.scene.SceneCanvas):
-    def __init__(self):
-        self.meshes = []
-        self.rotation = AffineTransform()
-
-        # Generate some data to work with
-        global mdata
-        mdata = create_sphere(20, 40, 1.0)
-
-        # Mesh with pre-indexed vertices, uniform color
-        verts = mdata.vertices(indexed='faces')
-        mesh = ModularMesh(pos=verts, color=(1, 0, 0, 1))
-        self.meshes.append(mesh)
-
-        # Mesh with pre-indexed vertices, per-face color
-        #   Because vertices are pre-indexed, we get a different color
-        #   every time a vertex is visited, resulting in sharp color
-        #   differences between edges.
-        nf = verts.size//9
-        fcolor = np.ones((nf, 3, 4), dtype=np.float32)
-        fcolor[..., 0] = np.linspace(1, 0, nf)[:, np.newaxis]
-        fcolor[..., 1] = np.random.normal(size=nf)[:, np.newaxis]
-        fcolor[..., 2] = np.linspace(0, 1, nf)[:, np.newaxis]
-        mesh = ModularMesh(pos=verts, color=fcolor)
-        self.meshes.append(mesh)
-
-        # Mesh with unindexed vertices, per-vertex color
-        #   Because vertices are unindexed, we get the same color
-        #   every time a vertex is visited, resulting in no color differences
-        #   between edges.
-        verts = mdata.vertices()
-        faces = mdata.faces()
-        nv = verts.size//3
-        vcolor = np.ones((nv, 4), dtype=np.float32)
-        vcolor[:, 0] = np.linspace(1, 0, nv)
-        vcolor[:, 1] = np.random.normal(size=nv)
-        vcolor[:, 2] = np.linspace(0, 1, nv)
-        mesh = ModularMesh(pos=verts, faces=faces, color=vcolor)
-        self.meshes.append(mesh)
-
-        # Mesh colored by vertices + grid contours
-        mesh = ModularMesh(pos=verts, faces=faces)
-        mesh.color_components = [VertexColorComponent(vcolor),
-                                 GridContourComponent(spacing=(0.13, 0.13,
-                                                               0.13))]
-        self.meshes.append(mesh)
-
-        # Phong shaded mesh
-        mesh = ModularMesh(pos=verts, faces=faces)
-        normal_comp = VertexNormalComponent(mdata)
-        mesh.color_components = [VertexColorComponent(vcolor),
-                                 GridContourComponent(spacing=(0.1, 0.1, 0.1)),
-                                 ShadingComponent(normal_comp,
-                                                  lights=[((-1, 1, -1),
-                                                          (1.0, 1.0, 1.0))],
-                                                  ambient=0.2)]
-        self.meshes.append(mesh)
-
-        # Phong shaded mesh, flat faces
-        mesh = ModularMesh(pos=mdata.vertices(indexed='faces'))
-        normal_comp = VertexNormalComponent(mdata, smooth=False)
-        mesh.color_components = [VertexColorComponent(vcolor[mdata.faces()]),
-                                 GridContourComponent(spacing=(0.1, 0.1, 0.1)),
-                                 ShadingComponent(normal_comp,
-                                                  lights=[((-1, 1, -1),
-                                                           (1.0, 1.0, 1.0))],
-                                                  ambient=0.2)]
-        self.meshes.append(mesh)
-
-        # Lay out meshes in a grid
-        grid = (3, 3)
-        s = 300. / max(grid)
-        for i, mesh in enumerate(self.meshes):
-            x = 800. * (i % grid[0]) / grid[0] + 400. / grid[0] - 2
-            y = 800. * (i // grid[1]) / grid[1] + 400. / grid[1] + 2
-            mesh.transform = ChainTransform([STTransform(translate=(x, y),
-                                                         scale=(s, s, 1)),
-                                             self.rotation])
-
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-
-        self.size = (800, 800)
-        self.show()
-
-        self.timer = vispy.app.Timer(connect=self.rotate)
-        self.timer.start(0.016)
-
-    def rotate(self, event):
-        self.rotation.rotate(1, (0, 1, 0))
-        # TODO: altering rotation should trigger this automatically.
-        for m in self.meshes:
-            m._program._need_build = True
-        self.update()
-
-    def on_draw(self, ev):
-        gloo.set_clear_color('black')
-        gloo.clear(color=True, depth=True)
-        for mesh in self.meshes:
-            self.draw_visual(mesh)
-
-
-if __name__ == '__main__':
-    win = Canvas()
-    import sys
-    if sys.flags.interactive != 1:
-        vispy.app.run()
diff --git a/examples/basics/visuals/modular_point.py b/examples/basics/visuals/modular_point.py
deleted file mode 100644
index 41e83be..0000000
--- a/examples/basics/visuals/modular_point.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-"""
-Simple demonstration of PointsVisual.
-"""
-
-import numpy as np
-import vispy.app
-from vispy import gloo
-from vispy.scene.visuals.modular_point import ModularPoint
-
-# vertex positions of data to draw
-N = 200
-pos = np.zeros((N, 3), dtype=np.float32)
-pos[:, 0] = np.linspace(50., 750., N)
-pos[:, 1] = np.random.normal(size=N, scale=100, loc=400).astype(np.float32)
-
-
-class Canvas(vispy.scene.SceneCanvas):
-    def __init__(self):
-        self.points = ModularPoint(pos, color=(0, 1, 0, 1))
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.size = (800, 800)
-        self.show()
-
-    def on_draw(self, ev):
-        gloo.set_clear_color('black')
-        gloo.clear(color=True, depth=True)
-        gloo.set_viewport(0, 0, *self.size)
-        self.draw_visual(self.points)
-
-
-if __name__ == '__main__':
-    win = Canvas()
-    import sys
-    if sys.flags.interactive != 1:
-        vispy.app.run()
diff --git a/examples/basics/visuals/polygon_visual.py b/examples/basics/visuals/polygon_visual.py
index 8096482..135eea3 100644
--- a/examples/basics/visuals/polygon_visual.py
+++ b/examples/basics/visuals/polygon_visual.py
@@ -1,15 +1,16 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
 Demonstration of Polygon and subclasses
 """
 
+import sys
 import numpy as np
-import vispy.app
-from vispy import gloo
-from vispy.scene import visuals, transforms
+
+from vispy import app, gloo, visuals
+from vispy.visuals import transforms
 
 # vertex positions of polygon data to draw
 pos = np.array([[0, 0, 0],
@@ -35,59 +36,62 @@ pos = np.array([[0, 0],
                 [0, 5]])
 
 theta = np.linspace(0, 2*np.pi, 11)
-pos = np.hstack([np.cos(theta)[:, np.newaxis], 
+pos = np.hstack([np.cos(theta)[:, np.newaxis],
                  np.sin(theta)[:, np.newaxis]])
 pos[::2] *= 0.4
 pos[-1] = pos[0]
 
 
-class Canvas(vispy.scene.SceneCanvas):
+class Canvas(app.Canvas):
     def __init__(self):
+        app.Canvas.__init__(self, keys='interactive', size=(800, 800))
         global pos
-        
         self.visuals = []
-        
-        polygon = visuals.Polygon(pos=pos, color=(0.8, .2, 0, 1),
-                                  border_color=(1, 1, 1, 1))
-        polygon.transform = transforms.STTransform(
-            scale=(200, 200),
-            translate=(600, 600))
+        polygon = visuals.PolygonVisual(pos=pos, color=(0.8, .2, 0, 1),
+                                        border_color=(1, 1, 1, 1))
+        polygon.transform = transforms.STTransform(scale=(200, 200),
+                                                   translate=(600, 600))
         self.visuals.append(polygon)
-        
-        ellipse = visuals.Ellipse(pos=(0, 0, 0), radius=(100, 150),
-                                  color=(0.2, 0.2, 0.8, 1),
-                                  border_color=(1, 1, 1, 1),
-                                  start_angle=180., span_angle=150.)
+
+        ellipse = visuals.EllipseVisual(pos=(0, 0, 0), radius=(100, 150),
+                                        color=(0.2, 0.2, 0.8, 1),
+                                        border_color=(1, 1, 1, 1),
+                                        start_angle=180., span_angle=150.)
         ellipse.transform = transforms.STTransform(scale=(0.9, 1.5),
                                                    translate=(200, 300))
         self.visuals.append(ellipse)
 
-        rect = visuals.Rectangle(pos=(600, 200, 0), height=200.,
-                                 width=300.,
-                                 radius=[30., 30., 0., 0.],
-                                 color=(0.5, 0.5, 0.2, 1),
-                                 border_color='white')
+        rect = visuals.RectangleVisual(pos=(600, 200, 0), height=200.,
+                                       width=300.,
+                                       radius=[30., 30., 0., 0.],
+                                       color=(0.5, 0.5, 0.2, 1),
+                                       border_color='white')
+        rect.transform = transforms.NullTransform()
         self.visuals.append(rect)
 
-        rpolygon = visuals.RegularPolygon(pos=(200., 600., 0), radius=160,
-                                          color=(0.2, 0.8, 0.2, 1),
-                                          border_color=(1, 1, 1, 1),
-                                          sides=6)
+        rpolygon = visuals.RegularPolygonVisual(pos=(200., 600., 0), 
+                                                radius=160,
+                                                color=(0.2, 0.8, 0.2, 1),
+                                                border_color=(1, 1, 1, 1),
+                                                sides=6)
+        rpolygon.transform = transforms.NullTransform()
         self.visuals.append(rpolygon)
-        
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.size = (800, 800)
+
+        for v in self.visuals:
+            v.tr_sys = transforms.TransformSystem(self)
+            v.tr_sys.visual_to_document = v.transform
+
         self.show()
-        
+
     def on_draw(self, ev):
         gloo.set_clear_color((0, 0, 0, 1))
+        gloo.set_viewport(0, 0, *self.physical_size)
         gloo.clear()
         for vis in self.visuals:
-            self.draw_visual(vis)
-        
+            vis.draw(vis.tr_sys)
+
 
 if __name__ == '__main__':
     win = Canvas() 
-    import sys
     if sys.flags.interactive != 1:
-        vispy.app.run()
+        win.app.run()
diff --git a/examples/basics/visuals/reactive_ellipse.py b/examples/basics/visuals/reactive_ellipse.py
deleted file mode 100644
index 1c402b3..0000000
--- a/examples/basics/visuals/reactive_ellipse.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-"""
-Simple demonstration of reactive EllipseVisual. 
-"""
-
-import vispy
-from vispy import gloo, app
-from vispy.scene import visuals
-
-
-class Canvas(vispy.scene.SceneCanvas):
-    def __init__(self):
-        self.ellipse = visuals.Ellipse(pos=(400, 400, 0), radius=[320, 240],
-                                       color=(1, 0, 0, 1),
-                                       border_color=(1, 1, 1, 1),
-                                       start_angle=180., span_angle=150.)
-        
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.size = (800, 800)
-        self.show()
-        
-        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
-
-    def on_timer(self, event):
-        self.ellipse.radius[0] += 1
-        self.ellipse.radius[1] += 1.5
-        self.ellipse.span_angle += 0.6
-        self.update()
-
-    def on_mouse_press(self, event):
-        self.ellipse.radius = [320, 240]
-        self.ellipse.span_angle = 150.
-        self.update()
-
-    def on_draw(self, ev):
-        gloo.clear(color='black')
-        self.draw_visual(self.ellipse)
-        
-
-if __name__ == '__main__':
-    win = Canvas() 
-    import sys
-    if sys.flags.interactive != 1:
-        vispy.app.run()
diff --git a/examples/basics/visuals/reactive_polygon.py b/examples/basics/visuals/reactive_polygon.py
deleted file mode 100644
index 54008e4..0000000
--- a/examples/basics/visuals/reactive_polygon.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-"""
-Simple demonstration of PolygonVisual. 
-"""
-
-import numpy as np
-import vispy
-from vispy import gloo, app
-from vispy.scene import visuals
-
-# vertex positions of data to draw
-pos = [[0, 0, 0],
-       [0.25, 0.22, 0],
-       [0.25, 0.5, 0],
-       [0, 0.5, 0],
-       [-0.25, 0.25, 0]]
-
-
-class Canvas(vispy.scene.SceneCanvas):
-    def __init__(self):
-        self.polygon = visuals.Polygon(pos=pos, color=(1, 0, 0, 1),
-                                       border_color=(1, 1, 1, 1))
-        self.polygon.transform = vispy.scene.transforms.STTransform(
-            scale=(500, 500),
-            translate=(400, 400))
-        
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.pos = np.array(pos)
-        self.i = 1
-        self.size = (800, 800)
-        self.show()
-
-        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
-
-    def on_timer(self, event):
-        self.pos[0] += [self.i, 0.0, 0.0]
-        self.i *= -0.92
-        self.polygon.pos = self.pos
-        self.update()
-
-    def on_mouse_press(self, event):
-        self.i = 1.
-        self.pos = np.array(pos)
-        self.update()
-
-    def on_draw(self, ev):
-        gloo.clear(color='black')
-        self.draw_visual(self.polygon)
-        
-
-if __name__ == '__main__':
-    win = Canvas() 
-    import sys
-    if sys.flags.interactive != 1:
-        vispy.app.run()
diff --git a/examples/basics/visuals/reactive_regular_polygon.py b/examples/basics/visuals/reactive_regular_polygon.py
deleted file mode 100644
index 5ce491e..0000000
--- a/examples/basics/visuals/reactive_regular_polygon.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-"""
-Simple demonstration of reactive RegularPolygonVisual. 
-"""
-
-import vispy
-from vispy import gloo, app
-from vispy.scene import visuals
-import numpy as np
-
-
-class Canvas(vispy.scene.SceneCanvas):
-    def __init__(self):
-        self.rpolygon = visuals.RegularPolygon(pos=(400.0, 400.0, 0), 
-                                               radius=80.,
-                                               color=(1, 0, 0, 1),
-                                               border_color=(1, 1, 1, 1),
-                                               sides=4)
-        
-        vispy.scene.SceneCanvas.__init__(self, keys='interactive')
-        self.size = (800, 800)
-        self.show()
-        
-        self.rfactor = 0.01
-        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
-
-    def on_timer(self, event):
-        if (self.rpolygon.radius > 400. or self.rpolygon.radius < 80.):
-            self.rfactor *= -1
-        self.rpolygon.radius += self.rfactor
-        self.rpolygon.sides += 100 * self.rfactor
-        self.rpolygon.color = (np.sin(self.rpolygon.radius * 0.00625), 0.5,
-                               np.cos(self.rpolygon.radius * 0.005), 1.0)
-        self.update()
-
-    def on_mouse_press(self, event):
-        self.rpolygon.radius = 80.
-        self.rpolygon.sides = 4
-        self.rpolygon.color = 'red'
-        self.update()
-
-    def on_draw(self, ev):
-        gloo.clear(color='black')
-        gloo.set_viewport(0, 0, *self.size)
-        self.draw_visual(self.rpolygon)
-        
-
-if __name__ == '__main__':
-    win = Canvas() 
-    import sys
-    if sys.flags.interactive != 1:
-        vispy.app.run()
diff --git a/examples/basics/visuals/rescalingmarkers.py b/examples/basics/visuals/rescalingmarkers.py
new file mode 100644
index 0000000..16983c1
--- /dev/null
+++ b/examples/basics/visuals/rescalingmarkers.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# vispy: gallery 30
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+""" Display markers at different sizes and line thicknessess.
+"""
+
+import numpy as np
+import vispy.scene as scene
+from vispy.scene import visuals
+import vispy.visuals as impl_visuals
+from vispy import app
+
+n = 500
+pos = np.zeros((n, 2))
+colors = np.ones((n, 4), dtype=np.float32)
+radius, theta, dtheta = 1.0, 0.0, 5.5 / 180.0 * np.pi
+for i in range(500):
+    theta += dtheta
+    x = radius * np.cos(theta)
+    y = radius * np.sin(theta)
+    r = 10.1 - i * 0.02
+    radius -= 0.45
+    pos[i] = x/512.+.5, 1.-(y/512.+.5)
+pos *= 512
+
+
+class Canvas(scene.SceneCanvas):
+
+    def __init__(self):
+        scene.SceneCanvas.__init__(
+            self,
+            keys='interactive', size=(512, 512),
+            title="Marker demo [press space to change marker]",
+            bgcolor='white'
+        )
+        self.index = 0
+        self.markers = visuals.Markers()
+        self.markers.set_data(pos, face_color=(0, 1, 0), scaling=False)
+        self.markers.set_symbol(impl_visuals.marker_types[self.index])
+        self.text = visuals.Text(impl_visuals.marker_types[self.index],
+                                 pos=(80, 15), font_size=14,
+                                 color='black', parent=self.scene)
+
+    def on_key_press(self, event):
+        if event.text == ' ':
+            self.index = (self.index + 1) % (len(impl_visuals.marker_types))
+            self.markers.set_symbol(impl_visuals.marker_types[self.index])
+            self.text.text = impl_visuals.marker_types[self.index]
+            self.update()
+
+
+canvas = Canvas()
+grid = canvas.central_widget.add_grid()
+vb1 = grid.add_view(row=0, col=0)
+vb1.add(canvas.markers)
+
+if __name__ == '__main__':
+    canvas.show()
+    app.run()
diff --git a/examples/basics/visuals/text_visual.py b/examples/basics/visuals/text_visual.py
index 1bb6b02..bacb085 100644
--- a/examples/basics/visuals/text_visual.py
+++ b/examples/basics/visuals/text_visual.py
@@ -1,24 +1,25 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
-from vispy import scene, gloo
-from vispy.scene.visuals import Text
+from vispy import app, gloo, visuals
 
 
-class Canvas(scene.SceneCanvas):
+class Canvas(app.Canvas):
     def __init__(self):
-        scene.SceneCanvas.__init__(self, title='Glyphs', keys='interactive')
+        app.Canvas.__init__(self, title='Glyphs', keys='interactive')
         self.font_size = 48.
-        self.text = Text('', bold=True)
+        self.text = visuals.TextVisual('', bold=True)
+        self.tr_sys = visuals.transforms.TransformSystem(self)
         self.apply_zoom()
 
     def on_draw(self, event):
         gloo.clear(color='white')
-        self.draw_visual(self.text)
+        gloo.set_viewport(0, 0, *self.physical_size)
+        self.text.draw(self.tr_sys)
 
     def on_mouse_wheel(self, event):
         """Use the mouse wheel to zoom."""
diff --git a/examples/basics/visuals/tube.py b/examples/basics/visuals/tube.py
new file mode 100644
index 0000000..c1301f1
--- /dev/null
+++ b/examples/basics/visuals/tube.py
@@ -0,0 +1,70 @@
+"""
+Demonstration of Tube
+"""
+
+import sys
+from vispy import scene
+from vispy.geometry.torusknot import TorusKnot
+
+from colorsys import hsv_to_rgb
+import numpy as np
+
+canvas = scene.SceneCanvas(keys='interactive')
+canvas.view = canvas.central_widget.add_view()
+
+points1 = TorusKnot(5, 3).first_component[:-1]
+points1[:, 0] -= 20.
+points1[:, 2] -= 15.
+
+points2 = points1.copy()
+points2[:, 2] += 30.
+
+points3 = points1.copy()
+points3[:, 0] += 41.
+points3[:, 2] += 30
+
+points4 = points1.copy()
+points4[:, 0] += 41.
+
+colors = np.linspace(0, 1, len(points1))
+colors = np.array([hsv_to_rgb(c, 1, 1) for c in colors])
+
+vertex_colors = np.random.random(8 * len(points1))
+vertex_colors = np.array([hsv_to_rgb(c, 1, 1) for c in vertex_colors])
+
+l1 = scene.visuals.Tube(points1,
+                        shading='flat',
+                        color=colors,  # this is overridden by
+                                       # the vertex_colors argument
+                        vertex_colors=vertex_colors,
+                        tube_points=8)
+
+l2 = scene.visuals.Tube(points2,
+                        color=['red', 'green', 'blue'],
+                        shading='smooth',
+                        tube_points=8)
+
+l3 = scene.visuals.Tube(points3,
+                        color=colors,
+                        shading='flat',
+                        tube_points=8,
+                        closed=True)
+
+l4 = scene.visuals.Tube(points4,
+                        color=colors,
+                        shading='flat',
+                        tube_points=8,
+                        mode='lines')
+
+canvas.view.add(l1)
+canvas.view.add(l2)
+canvas.view.add(l3)
+canvas.view.add(l4)
+canvas.view.camera = scene.TurntableCamera()
+# tube does not expose its limits yet
+canvas.view.camera.set_range((-20, 20), (-20, 20), (-20, 20))
+canvas.show()
+
+if __name__ == '__main__':
+    if sys.flags.interactive != 1:
+        canvas.app.run()
diff --git a/examples/basics/visuals/visual_filters.py b/examples/basics/visuals/visual_filters.py
new file mode 100644
index 0000000..8db1382
--- /dev/null
+++ b/examples/basics/visuals/visual_filters.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+# vispy: gallery 1
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+Demonstration of Line visual with arbitrary transforms.
+
+Several Line visuals are displayed that all have the same vertex position
+information, but different transformations.
+"""
+
+import numpy as np
+from vispy import app, gloo, visuals
+from vispy.visuals.transforms import STTransform
+from vispy.visuals.components import Clipper, Alpha, ColorFilter
+from vispy.visuals.shaders import Function
+from vispy.geometry import Rect
+
+# vertex positions of data to draw
+N = 400
+pos = np.zeros((N, 2), dtype=np.float32)
+pos[:, 0] = np.linspace(0, 350, N)
+pos[:, 1] = np.random.normal(size=N, scale=20, loc=0)
+
+
+class Canvas(app.Canvas):
+    def __init__(self):
+
+        # Define several Line visuals that use the same position data
+        self.lines = [visuals.LineVisual(pos=pos)
+                      for i in range(6)]
+
+        self.lines[0].transform = STTransform(translate=(0, 50))
+        
+        # Clipping filter (requires update when window is resized) 
+        self.lines[1].transform = STTransform(translate=(400, 50))
+        self.clipper = Clipper([500, 725, 200, 50])
+        self.lines[1].attach(self.clipper)
+        
+        # Opacity filter
+        self.lines[2].transform = STTransform(translate=(0, 150))
+        self.lines[2].attach(Alpha(0.4))
+        
+        # Color filter (for anaglyph stereo)
+        self.lines[3].transform = STTransform(translate=(400, 150))
+        self.lines[3].attach(ColorFilter([1, 0, 0, 1]))
+        
+        # A custom filter
+        class Hatching(object):
+            def __init__(self):
+                self.shader = Function("""
+                    void screen_filter() {
+                        float f = gl_FragCoord.x * 0.4 + gl_FragCoord.y;
+                        f = mod(f, 20);
+                        
+                        if( f < 5.0 ) {
+                            discard;
+                        }
+                        
+                        if( f < 20.0 ) {
+                            gl_FragColor.g = gl_FragColor.g + 0.05 * (20-f);
+                        }
+                    }
+                """)
+            
+            def _attach(self, visual):
+                visual._get_hook('frag', 'post').add(self.shader())
+
+        self.lines[4].transform = STTransform(translate=(0, 250))
+        self.lines[4].attach(Hatching())
+        
+        # Mixing filters
+        self.lines[5].transform = STTransform(translate=(400, 250))
+        self.lines[5].attach(ColorFilter([1, 0, 0, 1]))
+        self.lines[5].attach(Hatching())
+        
+        app.Canvas.__init__(self, keys='interactive', size=(800, 800))
+        
+        for line in self.lines:
+            tr_sys = visuals.transforms.TransformSystem(self)
+            tr_sys.visual_to_document = line.transform
+            line.tr_sys = tr_sys
+
+        self.show(True)
+
+    def on_draw(self, ev):
+        gloo.clear('black', depth=True)
+        gloo.set_viewport(0, 0, *self.physical_size)
+        for line in self.lines:
+            line.draw(line.tr_sys)
+
+    def on_resize(self, event):
+        for line in self.lines:
+            # let the transform systems know that the window has resized
+            line.tr_sys.auto_configure()
+            
+        # Need to update clipping boundaries if the window resizes.
+        trs = self.lines[1].tr_sys
+        tr = trs.document_to_framebuffer * trs.visual_to_document
+        self.clipper.bounds = tr.map(Rect(50, -15, 250, 30))
+
+
+if __name__ == '__main__':
+    win = Canvas()
+    import sys
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/examples/benchmark/scene_test_1.py b/examples/benchmark/scene_test_1.py
new file mode 100644
index 0000000..3aa6009
--- /dev/null
+++ b/examples/benchmark/scene_test_1.py
@@ -0,0 +1,355 @@
+# -*- coding: utf-8 -*-
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Compare an optimal pan/zoom implementation to the same functionality
+provided by scenegraph.
+
+Use --vispy-cprofile to see an overview of time spent in all functions.
+Use util.profiler and --vispy-profile=ClassName.method_name for more directed
+profiling measurements.
+"""
+import numpy as np
+import math
+
+from vispy import gloo, app, scene
+from vispy.visuals import Visual
+from vispy.visuals.shaders import ModularProgram, Function, Variable
+from vispy.visuals.transforms import TransformSystem, BaseTransform
+from vispy.util.profiler import Profiler
+
+
+class PanZoomTransform(BaseTransform):
+    glsl_map = """
+        vec4 pz_transform_map(vec2 pos) {
+            return vec4($zoom * (pos + $pan), 0, 1);
+        }
+    """
+
+    glsl_imap = """
+        vec4 pz_transform_imap(vec2 pos) {
+            return vec4(pos / $zoom - $pan, 0, 1);
+        }
+    """
+
+    Linear = True
+    Orthogonal = True
+    NonScaling = False
+    Isometric = False
+
+    def __init__(self):
+        super(PanZoomTransform, self).__init__()
+        self._pan = None
+        self._zoom = None
+
+    @property
+    def pan(self):
+        if isinstance(self._pan, Variable):
+            return np.array(self._pan.value, dtype=np.float32)
+        else:
+            raise NotImplementedError()
+
+    @pan.setter
+    def pan(self, value):
+        if isinstance(value, Variable):
+            self._pan = value
+            self._shader_map['pan'] = self._pan
+        elif isinstance(self._pan, Variable):
+            self._pan.value = value
+        else:
+            raise NotImplementedError()
+
+    @property
+    def zoom(self):
+        if isinstance(self._zoom, Variable):
+            return np.array(self._zoom.value, dtype=np.float32)
+        else:
+            raise NotImplementedError()
+
+    @zoom.setter
+    def zoom(self, value):
+        if isinstance(value, Variable):
+            self._zoom = value
+            self._shader_map['zoom'] = self._zoom
+        elif isinstance(self._zoom, Variable):
+            self._zoom.value = value
+        else:
+            raise NotImplementedError()
+
+    def map(self, coords):
+        if not isinstance(coords, np.ndarray):
+            coords = np.array(coords)
+        return self.zoom[None, :] * (coords + self.pan[None, :])
+
+    def imap(self, coords):
+        if not isinstance(coords, np.ndarray):
+            coords = np.array(coords)
+        return (coords / self.zoom[None, :]) - self.pan[None, :]
+
+
+class PanZoomCanvas(app.Canvas):
+    def __init__(self, **kwargs):
+        super(PanZoomCanvas, self).__init__(keys='interactive', **kwargs)
+        self._visuals = []
+
+        self._pz = PanZoomTransform()
+        self._pz.pan = Variable('uniform vec2 u_pan', (0, 0))
+        self._pz.zoom = Variable('uniform vec2 u_zoom', (1, 1))
+
+        self.width, self.height = self.size
+        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])
+        gloo.set_state(clear_color='black', blend=True,
+                       blend_func=('src_alpha', 'one_minus_src_alpha'))
+
+        self._tr = TransformSystem(self)
+        self.show()
+
+    def on_resize(self, event):
+        self.width, self.height = event.size
+        gloo.set_viewport(0, 0, event.physical_size[0], event.physical_size[1])
+
+    def _normalize(self, x_y):
+        x, y = x_y
+        w, h = float(self.width), float(self.height)
+        return x/(w/2.)-1., y/(h/2.)-1.
+
+    def bounds(self):
+        pan_x, pan_y = self._pz.pan
+        zoom_x, zoom_y = self._pz.zoom
+        xmin = -1 / zoom_x - pan_x
+        xmax = +1 / zoom_x - pan_x
+        ymin = -1 / zoom_y - pan_y
+        ymax = +1 / zoom_y - pan_y
+        return (xmin, ymin, xmax, ymax)
+
+    def on_mouse_move(self, event):
+        if event.is_dragging and not event.modifiers:
+            x0, y0 = self._normalize(event.press_event.pos)
+            x1, y1 = self._normalize(event.last_event.pos)
+            x, y = self._normalize(event.pos)
+            dx, dy = x - x1, -(y - y1)
+            button = event.press_event.button
+
+            pan_x, pan_y = self._pz.pan
+            zoom_x, zoom_y = self._pz.zoom
+
+            if button == 1:
+                self._pz.pan = (pan_x + dx/zoom_x,
+                                pan_y + dy/zoom_y)
+            elif button == 2:
+                zoom_x_new, zoom_y_new = (zoom_x * math.exp(2.5 * dx),
+                                          zoom_y * math.exp(2.5 * dy))
+                self._pz.zoom = (zoom_x_new, zoom_y_new)
+                self._pz.pan = (pan_x - x0 * (1./zoom_x - 1./zoom_x_new),
+                                pan_y + y0 * (1./zoom_y - 1./zoom_y_new))
+            self.update()
+
+    def on_mouse_wheel(self, event):
+        prof = Profiler()  # noqa
+        if not event.modifiers:
+            dx = np.sign(event.delta[1])*.05
+            x0, y0 = self._normalize(event.pos)
+            pan_x, pan_y = self._pz.pan
+            zoom_x, zoom_y = self._pz.zoom
+            zoom_x_new, zoom_y_new = (zoom_x * math.exp(2.5 * dx),
+                                      zoom_y * math.exp(2.5 * dx))
+            self._pz.zoom = (zoom_x_new, zoom_y_new)
+            self._pz.pan = (pan_x - x0 * (1./zoom_x - 1./zoom_x_new),
+                            pan_y + y0 * (1./zoom_y - 1./zoom_y_new))
+            self.update()
+
+    def on_key_press(self, event):
+        if event.key == 'R':
+            self._pz.zoom = (1., 1.)
+            self._pz.pan = (0., 0.)
+            self.update()
+
+    def add_visual(self, name, value):
+        value._program.vert['transform'] = self._pz
+        value.events.update.connect(self.update)
+        self._visuals.append(value)
+
+    def __setattr__(self, name, value):
+        if isinstance(value, Visual):
+            self.add_visual(name, value)
+        super(PanZoomCanvas, self).__setattr__(name, value)
+
+    @property
+    def visuals(self):
+        return self._visuals
+
+    def on_draw(self, event):
+        prof = Profiler()
+        gloo.clear()
+        for visual in self.visuals:
+            visual.draw(self._tr)
+            prof('draw visual')
+
+
+X_TRANSFORM = """
+float get_x(float x_index) {
+    // 'x_index' is between 0 and nsamples.
+    return -1. + 2. * x_index / (float($nsamples) - 1.);
+}
+"""
+
+Y_TRANSFORM = """
+float get_y(float y_index, float sample) {
+    // 'y_index' is between 0 and nsignals.
+    float a = float($scale) / float($nsignals);
+    float b = -1. + 2. * (y_index + .5) / float($nsignals);
+
+    return a * sample + b;
+}
+"""
+
+DISCRETE_CMAP = """
+vec3 get_color(float index) {
+    float x = (index + .5) / float($ncolors);
+    return texture2D($colormap, vec2(x, .5)).rgb;
+}
+"""
+
+
+class SignalsVisual(Visual):
+    VERTEX_SHADER = """
+    attribute float a_position;
+
+    attribute vec2 a_index;
+    varying vec2 v_index;
+
+    uniform float u_nsignals;
+    uniform float u_nsamples;
+
+    void main() {
+        vec2 position = vec2($get_x(a_index.y),
+                             $get_y(a_index.x, a_position));
+        gl_Position = $transform(position);
+
+        v_index = a_index;
+    }
+    """
+
+    FRAGMENT_SHADER = """
+    varying vec2 v_index;
+
+    void main() {
+        gl_FragColor = vec4($get_color(v_index.x), 1.);
+
+        // Discard vertices between two signals.
+        if ((fract(v_index.x) > 0.))
+            discard;
+    }
+    """
+
+    def __init__(self, data):
+        super(SignalsVisual, self).__init__()
+
+        self._program = ModularProgram(self.VERTEX_SHADER,
+                                       self.FRAGMENT_SHADER)
+
+        nsignals, nsamples = data.shape
+        # nsamples, nsignals = data.shape
+
+        self._data = data
+
+        a_index = np.c_[np.repeat(np.arange(nsignals), nsamples),
+                        np.tile(np.arange(nsamples), nsignals)
+                        ].astype(np.float32)
+
+        # Doesn't seem to work nor to be very efficient.
+        # indices = nsignals * np.arange(nsamples)
+        # indices = indices[None, :] + np.arange(nsignals)[:, None]
+        # indices = indices.flatten().astype(np.uint32)
+        # self._ibuffer = gloo.IndexBuffer(indices)
+
+        self._buffer = gloo.VertexBuffer(data.reshape(-1, 1))
+        self._program['a_position'] = self._buffer
+        self._program['a_index'] = a_index
+
+        x_transform = Function(X_TRANSFORM)
+        x_transform['nsamples'] = nsamples
+        self._program.vert['get_x'] = x_transform
+
+        y_transform = Function(Y_TRANSFORM)
+        y_transform['scale'] = Variable('uniform float u_signal_scale', 5.)
+        y_transform['nsignals'] = nsignals
+        self._program.vert['get_y'] = y_transform
+        self._y_transform = y_transform
+
+        colormap = Function(DISCRETE_CMAP)
+        cmap = np.random.uniform(size=(1, nsignals, 3),
+                                 low=.5, high=.9).astype(np.float32)
+        tex = gloo.Texture2D((cmap * 255).astype(np.uint8))
+        colormap['colormap'] = Variable('uniform sampler2D u_colormap', tex)
+        colormap['ncolors'] = nsignals
+        self._program.frag['get_color'] = colormap
+
+    @property
+    def data(self):
+        return self._data
+
+    @data.setter
+    def data(self, value):
+        self._data = value
+        self._buffer.set_subdata(value.reshape(-1, 1))
+        self.update()
+
+    @property
+    def signal_scale(self):
+        return self._y_transform['scale'].value
+
+    @signal_scale.setter
+    def signal_scale(self, value):
+        self._y_transform['scale'].value = value
+        self.update()
+
+    def draw(self, transform_system):
+        self._program.draw('line_strip')
+
+
+class Signals(SignalsVisual, scene.visuals.Node):
+    VERTEX_SHADER = """
+    attribute float a_position;
+
+    attribute vec2 a_index;
+    varying vec2 v_index;
+
+    uniform float u_nsignals;
+    uniform float u_nsamples;
+
+    void main() {
+        vec4 position = vec4($get_x(a_index.y),
+                             $get_y(a_index.x, a_position), 0, 1);
+        gl_Position = $transform(position);
+
+        v_index = a_index;
+    }
+    """
+
+    def draw(self, transform_system):
+        self._program.vert['transform'] = transform_system.get_full_transform()
+        self._program.draw('line_strip')
+
+
+if __name__ == '__main__':
+    data = np.random.normal(size=(128, 1000)).astype(np.float32)
+
+    pzcanvas = PanZoomCanvas(position=(400, 300), size=(800, 600),
+                             title="PanZoomCanvas")
+    visual = SignalsVisual(data)
+    pzcanvas.add_visual('signal', visual)
+
+    scanvas = scene.SceneCanvas(show=True, keys='interactive',
+                                title="SceneCanvas")
+    svisual = Signals(data)
+    view = scanvas.central_widget.add_view()
+    view.add(svisual)
+    view.camera = 'panzoom'
+
+    import sys
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/examples/benchmark/scene_test_2.py b/examples/benchmark/scene_test_2.py
new file mode 100644
index 0000000..d64c814
--- /dev/null
+++ b/examples/benchmark/scene_test_2.py
@@ -0,0 +1,193 @@
+# -*- coding: utf-8 -*-
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Compare an optimal plot grid implementation to the same functionality
+provided by scenegraph.
+
+Use --vispy-cprofile to see an overview of time spent in all functions.
+Use util.profiler and --vispy-profile=ClassName.method_name for more directed
+profiling measurements.
+"""
+from __future__ import division
+import numpy as np
+
+from vispy import gloo, app, scene, visuals
+from vispy.util.profiler import Profiler
+
+
+class GridCanvas(app.Canvas):
+    def __init__(self, cells, **kwargs):
+        m, n = (10, 10)
+        self.grid_size = (m, n)
+        self.cells = cells
+        super(GridCanvas, self).__init__(keys='interactive',
+                                         show=True, **kwargs)
+
+    def on_initialize(self, event):
+        gloo.set_state(clear_color='black', blend=True,
+                       blend_func=('src_alpha', 'one_minus_src_alpha'))
+
+    def on_mouse_move(self, event):
+        if event.is_dragging and not event.modifiers:
+            dx = (event.pos - event.last_event.pos) * [1, -1]
+            i, j = event.press_event.pos / self.size
+            m, n = len(self.cells), len(self.cells[0])
+            cell = self.cells[int(i*m)][n - 1 - int(j*n)]
+            if event.press_event.button == 1:
+                offset = (np.array(cell.offset) + 
+                          (dx / (np.array(self.size) / [m, n])) *  
+                          (2 / np.array(cell.scale)))
+                cell.set_transform(offset, cell.scale)
+                
+            else:
+                cell.set_transform(cell.offset, cell.scale * 1.05 ** dx)
+            self.update()
+
+    def on_draw(self, event):
+        prof = Profiler()  # noqa
+        gloo.clear()
+        M = len(self.cells)
+        N = len(self.cells[0])
+        w, h = self.size
+        for i in range(M):
+            for j in range(N):
+                gloo.set_viewport(w*i/M, h*j/N, w/M, h/N)
+                self.cells[i][j].draw()
+
+
+vert = """
+attribute vec2 pos;
+uniform vec2 offset;
+uniform vec2 scale;
+
+void main() {
+    gl_Position = vec4((pos + offset) * scale, 0, 1);
+}
+"""
+
+frag = """
+void main() {
+    gl_FragColor = vec4(1, 1, 1, 0.5);
+}
+"""
+
+
+class Line(object):
+    def __init__(self, data, offset, scale):
+        self.data = gloo.VertexBuffer(data)
+        self.program = gloo.Program(vert, frag)
+        self.program['pos'] = self.data
+        self.set_transform(offset, scale)
+        
+    def set_transform(self, offset, scale):
+        self.offset = offset
+        self.scale = scale
+        self.program['offset'] = self.offset
+        self.program['scale'] = self.scale
+    
+    def draw(self):
+        self.program.draw('line_strip')
+
+
+class VisualCanvas(app.Canvas):
+    def __init__(self, vis, **kwargs):
+        super(VisualCanvas, self).__init__(keys='interactive',
+                                           show=True, **kwargs)
+        m, n = (10, 10)
+        self.grid_size = (m, n)
+        self.visuals = vis
+        for row in vis:
+            for v in row:
+                v.tr_sys = visuals.transforms.TransformSystem(self)
+                v.tr_sys.visual_to_document = v.transform
+
+    def on_initialize(self, event):
+        gloo.set_state(clear_color='black', blend=True,
+                       blend_func=('src_alpha', 'one_minus_src_alpha'))
+
+    def on_mouse_move(self, event):
+        if event.is_dragging and not event.modifiers:
+            dx = (event.pos - event.last_event.pos)
+            x, y = event.press_event.pos / self.size
+            m, n = self.grid_size
+            i, j = int(x*m), n - 1 - int(y*n)
+            v = self.visuals[i][j]
+            tr = v.transform
+            if event.press_event.button == 1:
+                offset = np.array(tr.translate)[:2] + dx
+                tr.translate = offset
+                
+            else:
+                tr.scale = tr.scale[:2] * 1.05 ** (dx * (1, -1))
+            self.update()
+
+    def on_draw(self, event):
+        prof = Profiler()  # noqa
+        gloo.clear()
+        M, N = self.grid_size
+        w, h = self.size
+        for i in range(M):
+            for j in range(N):
+                gloo.set_viewport(w*i/M, h*j/N, w/M, h/N)
+                v = self.visuals[i][j]
+                v.draw(v.tr_sys)
+
+
+if __name__ == '__main__':
+    M, N = (10, 10)
+    
+    data = np.empty((10000, 2), dtype=np.float32)
+    data[:, 0] = np.linspace(0, 100, data.shape[0])
+    data[:, 1] = np.random.normal(size=data.shape[0])
+    
+    # Optimized version
+    cells = []
+    for i in range(M):
+        row = []
+        cells.append(row)
+        for j in range(N):
+            row.append(Line(data, offset=(-50, 0), scale=(1.9/100, 2/10)))
+    
+    gcanvas = GridCanvas(cells, position=(400, 300), size=(800, 600), 
+                         title="GridCanvas")
+    
+    # Visual version
+    vlines = []
+    for i in range(M):
+        row = []
+        vlines.append(row)
+        for j in range(N):
+            v = visuals.LineVisual(pos=data, color=(1, 1, 1, 0.5), method='gl')
+            v.transform = visuals.transforms.STTransform(translate=(0, 200), 
+                                                         scale=(7, 50))
+            row.append(v)
+    
+    vcanvas = VisualCanvas(vlines, position=(400, 300), size=(800, 600), 
+                           title="VisualCanvas")
+    
+    # Scenegraph version
+    scanvas = scene.SceneCanvas(show=True, keys='interactive', 
+                                title="SceneCanvas")
+    
+    scanvas.size = 800, 600
+    scanvas.show()
+    grid = scanvas.central_widget.add_grid()
+
+    lines = []
+    for i in range(10):
+        lines.append([])
+        for j in range(10):
+            vb = grid.add_view(row=i, col=j)
+            vb.camera.rect = (0, -5), (100, 10)
+            vb.border = (1, 1, 1, 0.4)
+            line = scene.visuals.Line(pos=data, color=(1, 1, 1, 0.5), 
+                                      method='gl')
+            vb.add(line)
+    
+    import sys
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/examples/benchmark/simple_glut.py b/examples/benchmark/simple_glut.py
index 5773d87..1ef06f9 100755
--- a/examples/benchmark/simple_glut.py
+++ b/examples/benchmark/simple_glut.py
@@ -1,7 +1,8 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
+# vispy: testskip
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 from vispy.gloo import gl
diff --git a/examples/benchmark/simple_vispy.py b/examples/benchmark/simple_vispy.py
index 1ca8d75..c2df288 100755
--- a/examples/benchmark/simple_vispy.py
+++ b/examples/benchmark/simple_vispy.py
@@ -1,16 +1,17 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
+import sys
 
 from vispy import app
 from vispy.gloo import clear
 
 # app.use_app('pyqt4')  # or pyside, glut, pyglet, sdl2, etc.
 
-canvas = app.Canvas(size=(512, 512), title = "Do nothing benchmark (vispy)",
+canvas = app.Canvas(size=(512, 512), title="Do nothing benchmark (vispy)",
                     keys='interactive')
 
 
@@ -19,6 +20,8 @@ def on_draw(event):
     clear(color=True, depth=True)
     canvas.update()  # Draw frames as fast as possible
 
-canvas.show()
-canvas.measure_fps()
-app.run()
+if __name__ == '__main__':
+    canvas.show()
+    canvas.measure_fps()
+    if sys.flags.interactive == 0:
+        app.run()
diff --git a/examples/collections/choropleth.py b/examples/collections/choropleth.py
new file mode 100644
index 0000000..694ac67
--- /dev/null
+++ b/examples/collections/choropleth.py
@@ -0,0 +1,84 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: testskip
+
+import json
+
+import numpy as np
+
+from vispy import app, gloo
+from vispy.util import load_data_file
+from vispy.visuals.collections import PathCollection, PolygonCollection
+from vispy.visuals.transforms import PanZoomTransform
+
+
+path = load_data_file('uscounties/uscounties.geojson')
+with open(path, 'r') as f:
+    geo = json.load(f)
+
+
+def unique_rows(data):
+    v = data.view(data.dtype.descr * data.shape[1])
+    _, idx = np.unique(v, return_index=True)
+    return data[np.sort(idx)]
+
+
+def add(P, color):
+    P = np.array(P)
+    if len(P) < 2:
+        return
+    P = np.array(P) / 20.0 + (5, -2)
+    p = np.zeros((len(P), 3))
+    p[:, :2] = P
+    p = unique_rows(p)
+    if len(p) > 1:
+        paths.append(p, closed=True)
+    if len(p) > 2:
+        polys.append(p, color=color)
+
+
+# Create the canvas.
+canvas = app.Canvas(size=(800, 800), keys='interactive')
+gloo.set_viewport(0, 0, canvas.size[0], canvas.size[1])
+gloo.set_state("translucent", depth_test=False)
+
+panzoom = PanZoomTransform(canvas, aspect=1)
+paths = PathCollection(mode="agg+", color="global", transform=panzoom)
+polys = PolygonCollection("raw", color="local", transform=panzoom)
+paths.update.connect(canvas.update)
+
+for feature in geo["features"]:
+    if feature["geometry"]["type"] == 'Polygon':
+        path = feature["geometry"]["coordinates"]
+        rgba = np.random.uniform(0.5, .8, 4)
+        rgba[3] = 1
+        add(path[0], color=rgba)
+
+    elif feature["geometry"]["type"] == 'MultiPolygon':
+        coordinates = feature["geometry"]["coordinates"]
+        for path in coordinates:
+            rgba = np.random.uniform(0.5, .8, 4)
+            rgba[3] = 1
+            add(path[0], color=rgba)
+
+paths["color"] = 0, 0, 0, 1
+paths["linewidth"] = 1.0
+paths['viewport'] = 0, 0, 800, 800
+
+
+ at canvas.connect
+def on_draw(e):
+    gloo.clear('white')
+    polys.draw()
+    paths.draw()
+
+
+ at canvas.connect
+def on_resize(event):
+    width, height = event.size
+    gloo.set_viewport(0, 0, width, height)
+    paths['viewport'] = 0, 0, width, height
+
+if __name__ == '__main__':
+    canvas.show()
+    app.run()
diff --git a/examples/collections/path_collection.py b/examples/collections/path_collection.py
new file mode 100644
index 0000000..b685d6a
--- /dev/null
+++ b/examples/collections/path_collection.py
@@ -0,0 +1,49 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: testskip
+
+import numpy as np
+from vispy import app, gloo
+from vispy.visuals.collections import PathCollection
+
+c = app.Canvas(size=(800, 800), show=True, keys='interactive')
+gloo.set_viewport(0, 0, c.size[0], c.size[1])
+gloo.set_state("translucent", depth_test=False)
+
+
+def star(inner=0.5, outer=1.0, n=5):
+    R = np.array([inner, outer] * n)
+    T = np.linspace(0, 2 * np.pi, 2 * n, endpoint=False)
+    P = np.zeros((2 * n, 3))
+    P[:, 0] = R * np.cos(T)
+    P[:, 1] = R * np.sin(T)
+    return P
+
+
+n = 2500
+S = star(n=5)
+P = np.tile(S.ravel(), n).reshape(n, len(S), 3)
+P *= np.random.uniform(5, 10, n)[:, np.newaxis, np.newaxis]
+P[:, :, :2] += np.random.uniform(0, 800, (n, 2))[:, np.newaxis, :]
+P = P.reshape(n * len(S), 3)
+P = 2 * (P / (800, 800, 1)) - 1
+
+paths = PathCollection(mode="agg")
+paths.append(P, closed=True, itemsize=len(S))
+paths["linewidth"] = 1.0
+paths['viewport'] = 0, 0, 800, 800
+
+
+ at c.connect
+def on_draw(e):
+    gloo.clear('white')
+    paths.draw()
+
+
+ at c.connect
+def on_resize(e):
+    width, height = e.size[0], e.size[1]
+    gloo.set_viewport(0, 0, width, height)
+    paths['viewport'] = 0, 0, width, height
+
+app.run()
diff --git a/examples/collections/point_collection.py b/examples/collections/point_collection.py
new file mode 100644
index 0000000..f2280c0
--- /dev/null
+++ b/examples/collections/point_collection.py
@@ -0,0 +1,35 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: testskip
+
+import sys
+import numpy as np
+from vispy import app, gloo
+from vispy.visuals.collections import PointCollection
+from vispy.visuals.transforms import PanZoomTransform
+
+canvas = app.Canvas(size=(800, 600), show=True, keys='interactive')
+gloo.set_viewport(0, 0, canvas.size[0], canvas.size[1])
+gloo.set_state("translucent", depth_test=False)
+
+panzoom = PanZoomTransform(canvas)
+
+points = PointCollection("agg", color="shared", transform=panzoom)
+points.append(np.random.normal(0.0, 0.5, (10000, 3)), itemsize=5000)
+points["color"] = (1, 0, 0, 1), (0, 0, 1, 1)
+points.update.connect(canvas.update)
+
+
+ at canvas.connect
+def on_draw(event):
+    gloo.clear('white')
+    points.draw()
+
+
+ at canvas.connect
+def on_resize(event):
+    width, height = event.size
+    gloo.set_viewport(0, 0, width, height)
+
+if __name__ == '__main__' and sys.flags.interactive == 0:
+    app.run()
diff --git a/examples/collections/polygon_collection.py b/examples/collections/polygon_collection.py
new file mode 100644
index 0000000..5938897
--- /dev/null
+++ b/examples/collections/polygon_collection.py
@@ -0,0 +1,54 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: testskip
+
+import numpy as np
+from vispy import app, gloo
+from vispy.visuals.collections import PathCollection, PolygonCollection
+
+
+canvas = app.Canvas(size=(800, 800), show=True, keys='interactive')
+gloo.set_viewport(0, 0, canvas.size[0], canvas.size[1])
+gloo.set_state("translucent", depth_test=False)
+
+
+def star(inner=0.5, outer=1.0, n=5):
+    R = np.array([inner, outer] * n)
+    T = np.linspace(0, 2 * np.pi, 2 * n, endpoint=False)
+    P = np.zeros((2 * n, 3))
+    P[:, 0] = R * np.cos(T)
+    P[:, 1] = R * np.sin(T)
+    return P
+
+paths = PathCollection("agg", color='shared')
+polys = PolygonCollection("raw", color='shared')
+
+P = star()
+
+n = 100
+for i in range(n):
+    c = i / float(n)
+    x, y = np.random.uniform(-1, +1, 2)
+    s = 100 / 800.0
+    polys.append(P * s + (x, y, i / 1000.), color=(1, 0, 0, .5))
+    paths.append(
+        P * s + (x, y, (i - 1) / 1000.), closed=True, color=(0, 0, 0, .5))
+
+paths["linewidth"] = 1.0
+paths['viewport'] = 0, 0, 800, 800
+
+
+ at canvas.connect
+def on_draw(e):
+    gloo.clear('white')
+    polys.draw()
+    paths.draw()
+
+
+ at canvas.connect
+def on_resize(event):
+    width, height = event.size
+    gloo.set_viewport(0, 0, width, height)
+    paths['viewport'] = 0, 0, width, height
+
+app.run()
diff --git a/examples/collections/segment_collection.py b/examples/collections/segment_collection.py
new file mode 100644
index 0000000..6641bc3
--- /dev/null
+++ b/examples/collections/segment_collection.py
@@ -0,0 +1,40 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: testskip
+
+import numpy as np
+from vispy import app, gloo
+from vispy.visuals.collections import SegmentCollection
+
+
+c = app.Canvas(size=(1200, 600), show=True, keys='interactive')
+gloo.set_viewport(0, 0, c.size[0], c.size[1])
+gloo.set_state("translucent", depth_test=False)
+
+segments = SegmentCollection("agg", linewidth="local")
+n = 100
+P0 = np.dstack(
+    (np.linspace(100, 1100, n), np.ones(n) * 50, np.zeros(n))).reshape(n, 3)
+P0 = 2 * (P0 / (1200, 600, 1)) - 1
+P1 = np.dstack(
+    (np.linspace(110, 1110, n), np.ones(n) * 550, np.zeros(n))).reshape(n, 3)
+P1 = 2 * (P1 / (1200, 600, 1)) - 1
+
+segments.append(P0, P1, linewidth=np.linspace(1, 8, n))
+segments['antialias'] = 1
+segments['viewport'] = 0, 0, 1200, 600
+
+
+ at c.connect
+def on_draw(e):
+    gloo.clear('white')
+    segments.draw()
+
+
+ at c.connect
+def on_resize(e):
+    width, height = e.size[0], e.size[1]
+    gloo.set_viewport(0, 0, width, height)
+    segments['viewport'] = 0, 0, width, height
+
+app.run()
diff --git a/examples/collections/tiger.py b/examples/collections/tiger.py
new file mode 100644
index 0000000..1182f74
--- /dev/null
+++ b/examples/collections/tiger.py
@@ -0,0 +1,75 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+# Distributed under the (new) BSD License.
+# -----------------------------------------------------------------------------
+
+from vispy import app, gloo
+from vispy.util import load_data_file
+from vispy.util.svg import Document
+from vispy.visuals.collections import PathCollection, PolygonCollection
+from vispy.visuals.transforms import PanZoomTransform
+
+path = load_data_file('tiger/tiger.svg')
+tiger = Document(path)
+
+width, height = int(tiger.viewport.width), int(tiger.viewport.height)
+canvas = app.Canvas(size=(width, height), show=True, keys='interactive')
+gloo.set_viewport(0, 0, width, height)
+gloo.set_state("translucent", depth_test=True)
+
+panzoom = PanZoomTransform(canvas, aspect=1.0)
+paths = PathCollection(
+    "agg+", linewidth='shared', color="shared", transform=panzoom)
+polys = PolygonCollection("agg", transform=panzoom)
+paths.update.connect(canvas.update)
+
+z = 0
+for path in tiger.paths:
+    for vertices, closed in path.vertices:
+
+        vertices = 2 * (vertices / (width, height, 1)) - 1
+        vertices[:, 1] = -vertices[:, 1]
+        if len(vertices) < 3:
+            continue
+        if path.style.stroke is not None:
+            vertices[:, 2] = z - 0.5 / 1000.
+            if path.style.stroke_width:
+                stroke_width = path.style.stroke_width.value
+            else:
+                stroke_width = 2.0
+            paths.append(vertices, closed=closed, color=path.style.stroke.rgba,
+                         linewidth=stroke_width)
+        if path.style.fill is not None:
+            if path.style.stroke is None:
+                vertices[:, 2] = z - 0.25 / 1000.
+                paths.append(vertices, closed=closed,
+                             color=path.style.fill.rgba,
+                             linewidth=1)
+            vertices[:, 2] = z
+            polys.append(vertices, color=path.style.fill.rgba)
+    z -= 1 / 1000.
+
+
+paths["linewidth"] = 1.0
+paths['viewport'] = 0, 0, 800, 800
+
+
+ at canvas.connect
+def on_draw(e):
+    gloo.clear('white')
+    polys.draw()
+    paths.draw()
+
+
+ at canvas.connect
+def on_resize(e):
+    width, height = e.size[0], e.size[1]
+    gloo.set_viewport(0, 0, width, height)
+    paths['viewport'] = 0, 0, width, height
+
+if __name__ == '__main__':
+    canvas.show()
+    app.run()
diff --git a/examples/collections/triangle_collection.py b/examples/collections/triangle_collection.py
new file mode 100644
index 0000000..844b66d
--- /dev/null
+++ b/examples/collections/triangle_collection.py
@@ -0,0 +1,69 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: testskip
+
+import numpy as np
+from vispy import app, gloo
+from vispy.geometry import Triangulation
+from vispy.visuals.collections import PathCollection, TriangleCollection
+
+canvas = app.Canvas(size=(800, 800), show=True, keys='interactive')
+gloo.set_viewport(0, 0, canvas.size[0], canvas.size[1])
+gloo.set_state("translucent", depth_test=False)
+
+
+def triangulate(P):
+    n = len(P)
+    S = np.repeat(np.arange(n + 1), 2)[1:-1]
+    S[-2:] = n - 1, 0
+    S = S.reshape(len(S) / 2, 2)
+    T = Triangulation(P[:, :2], S)
+    T.triangulate()
+    points = T.pts
+    triangles = T.tris.ravel()
+    P = np.zeros((len(points), 3), dtype=np.float32)
+    P[:, :2] = points
+    return P, triangles
+
+
+def star(inner=0.5, outer=1.0, n=5):
+    R = np.array([inner, outer] * n)
+    T = np.linspace(0, 2 * np.pi, 2 * n, endpoint=False)
+    P = np.zeros((2 * n, 3))
+    P[:, 0] = R * np.cos(T)
+    P[:, 1] = R * np.sin(T)
+    return P
+
+paths = PathCollection("agg", color='shared')
+triangles = TriangleCollection("raw", color='shared')
+
+P0 = star()
+P1, I = triangulate(P0)
+
+n = 1000
+for i in range(n):
+    c = i / float(n)
+    x, y = np.random.uniform(-1, +1, 2)
+    s = 25 / 800.0
+    triangles.append(P1 * s + (x, y, i / 1000.), I, color=(1, 0, 0, .5))
+    paths.append(
+        P0 * s + (x, y, (i - 1) / 1000.), closed=True, color=(0, 0, 0, .5))
+
+paths["linewidth"] = 1.0
+paths['viewport'] = 0, 0, 800, 800
+
+
+ at canvas.connect
+def on_draw(e):
+    gloo.clear('white')
+    triangles.draw()
+    paths.draw()
+
+
+ at canvas.connect
+def on_resize(event):
+    width, height = event.size
+    gloo.set_viewport(0, 0, width, height)
+    paths['viewport'] = 0, 0, width, height
+
+app.run()
diff --git a/examples/demo/gloo/atom.py b/examples/demo/gloo/atom.py
index 0e4a254..5f2025d 100644
--- a/examples/demo/gloo/atom.py
+++ b/examples/demo/gloo/atom.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
@@ -102,16 +102,15 @@ void main()
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-        self.size = 800, 800
+        app.Canvas.__init__(self, keys='interactive', size=(800, 800))
         self.title = "Atom [zoom with mouse scroll"
 
+        self.translate = 6.5
         self.program = gloo.Program(vert, frag)
-        self.view = np.eye(4, dtype=np.float32)
+        self.view = translate((0, 0, -self.translate))
         self.model = np.eye(4, dtype=np.float32)
         self.projection = np.eye(4, dtype=np.float32)
-        self.translate = 6.5
-        translate(self.view, 0, 0, -self.translate)
+        self.apply_zoom()
 
         self.program.bind(gloo.VertexBuffer(data))
         self.program['u_model'] = self.model
@@ -123,10 +122,12 @@ class Canvas(app.Canvas):
         self.clock = 0
         self.stop_rotation = False
 
+        gloo.set_state('translucent', depth_test=False)
+        self.program['u_clock'] = 0.0
+
         self._timer = app.Timer('auto', connect=self.on_timer, start=True)
 
-    def on_initialize(self, event):
-        gloo.set_state('translucent', depth_test=False)
+        self.show()
 
     def on_key_press(self, event):
         if event.text == ' ':
@@ -136,26 +137,20 @@ class Canvas(app.Canvas):
         if not self.stop_rotation:
             self.theta += .05
             self.phi += .05
-            self.model = np.eye(4, dtype=np.float32)
-            rotate(self.model, self.theta, 0, 0, 1)
-            rotate(self.model, self.phi, 0, 1, 0)
+            self.model = np.dot(rotate(self.theta, (0, 0, 1)),
+                                rotate(self.phi, (0, 1, 0)))
             self.program['u_model'] = self.model
         self.clock += np.pi / 100
         self.program['u_clock'] = self.clock
         self.update()
 
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = perspective(45.0, width / float(height), 1.0, 1000.0)
-        self.program['u_projection'] = self.projection
+        self.apply_zoom()
 
     def on_mouse_wheel(self, event):
         self.translate += event.delta[1]
         self.translate = max(2, self.translate)
-        self.view = np.eye(4, dtype=np.float32)
-        translate(self.view, 0, 0, -self.translate)
-
+        self.view = translate((0, 0, -self.translate))
         self.program['u_view'] = self.view
         self.program['u_size'] = 5 / self.translate
         self.update()
@@ -164,8 +159,13 @@ class Canvas(app.Canvas):
         gloo.clear('black')
         self.program.draw('points')
 
+    def apply_zoom(self):
+        width, height = self.physical_size
+        gloo.set_viewport(0, 0, width, height)
+        self.projection = perspective(45.0, width / float(height), 1.0, 1000.0)
+        self.program['u_projection'] = self.projection
+
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/boids.py b/examples/demo/gloo/boids.py
index 2d62b15..c10e223 100644
--- a/examples/demo/gloo/boids.py
+++ b/examples/demo/gloo/boids.py
@@ -17,29 +17,6 @@ from scipy.spatial import cKDTree
 from vispy import gloo
 from vispy import app
 
-# Create boids
-n = 1000
-particles = np.zeros(2 + n, [('position', 'f4', 3),
-                             ('position_1', 'f4', 3),
-                             ('position_2', 'f4', 3),
-                             ('velocity', 'f4', 3),
-                             ('color', 'f4', 4),
-                             ('size', 'f4', 1)])
-boids = particles[2:]
-target = particles[0]
-predator = particles[1]
-
-boids['position'] = np.random.uniform(-0.25, +0.25, (n, 3))
-boids['velocity'] = np.random.uniform(-0.00, +0.00, (n, 3))
-boids['size'] = 4
-boids['color'] = 1, 1, 1, 1
-
-target['size'] = 16
-target['color'][:] = 1, 1, 0, 1
-predator['size'] = 16
-predator['color'][:] = 1, 0, 0, 1
-target['position'][:] = 0.25, 0.0, 0
-
 VERT_SHADER = """
 #version 120
 attribute vec3 position;
@@ -73,32 +50,62 @@ class Canvas(app.Canvas):
     def __init__(self):
         app.Canvas.__init__(self, keys='interactive')
 
+        ps = self.pixel_scale
+
+        # Create boids
+        n = 1000
+        self.particles = np.zeros(2 + n, [('position', 'f4', 3),
+                                          ('position_1', 'f4', 3),
+                                          ('position_2', 'f4', 3),
+                                          ('velocity', 'f4', 3),
+                                          ('color', 'f4', 4),
+                                          ('size', 'f4', 1*ps)])
+        self.boids = self.particles[2:]
+        self.target = self.particles[0]
+        self.predator = self.particles[1]
+
+        self.boids['position'] = np.random.uniform(-0.25, +0.25, (n, 3))
+        self.boids['velocity'] = np.random.uniform(-0.00, +0.00, (n, 3))
+        self.boids['size'] = 4*ps
+        self.boids['color'] = 1, 1, 1, 1
+
+        self.target['size'] = 16*ps
+        self.target['color'][:] = 1, 1, 0, 1
+        self.predator['size'] = 16*ps
+        self.predator['color'][:] = 1, 0, 0, 1
+        self.target['position'][:] = 0.25, 0.0, 0
+
         # Time
         self._t = time.time()
         self._pos = 0.0, 0.0
         self._button = None
 
+        width, height = self.physical_size
+        gloo.set_viewport(0, 0, width, height)
+
         # Create program
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
 
         # Create vertex buffers
-        self.vbo_position = gloo.VertexBuffer(particles['position'].copy())
-        self.vbo_color = gloo.VertexBuffer(particles['color'].copy())
-        self.vbo_size = gloo.VertexBuffer(particles['size'].copy())
+        self.vbo_position = gloo.VertexBuffer(self.particles['position']
+                                              .copy())
+        self.vbo_color = gloo.VertexBuffer(self.particles['color'].copy())
+        self.vbo_size = gloo.VertexBuffer(self.particles['size'].copy())
 
         # Bind vertex buffers
         self.program['color'] = self.vbo_color
         self.program['size'] = self.vbo_size
         self.program['position'] = self.vbo_position
-        
-        self._timer = app.Timer('auto', connect=self.update, start=True)
-    
-    def on_initialize(self, event):
+
         gloo.set_state(clear_color=(0, 0, 0, 1), blend=True,
                        blend_func=('src_alpha', 'one'))
 
+        self._timer = app.Timer('auto', connect=self.update, start=True)
+
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
+        width, height = event.physical_size
         gloo.set_viewport(0, 0, width, height)
 
     def on_mouse_press(self, event):
@@ -118,9 +125,9 @@ class Canvas(app.Canvas):
         sy = - (2 * y / float(h) - 1.0)
 
         if self._button == 1:
-            target['position'][:] = sx, sy, 0
+            self.target['position'][:] = sx, sy, 0
         elif self._button == 2:
-            predator['position'][:] = sx, sy, 0
+            self.predator['position'][:] = sx, sy, 0
 
     def on_draw(self, event):
         gloo.clear()
@@ -133,16 +140,16 @@ class Canvas(app.Canvas):
         t = self._t
 
         t += 0.5 * dt
-        #target[...] = np.array([np.sin(t),np.sin(2*t),np.cos(3*t)])*.1
+        #self.target[...] = np.array([np.sin(t),np.sin(2*t),np.cos(3*t)])*.1
 
         t += 0.5 * dt
-        #predator[...] = np.array([np.sin(t),np.sin(2*t),np.cos(3*t)])*.2
+        #self.predator[...] = np.array([np.sin(t),np.sin(2*t),np.cos(3*t)])*.2
 
-        boids['position_2'] = boids['position_1']
-        boids['position_1'] = boids['position']
-        n = len(boids)
-        P = boids['position']
-        V = boids['velocity']
+        self.boids['position_2'] = self.boids['position_1']
+        self.boids['position_1'] = self.boids['position']
+        n = len(self.boids)
+        P = self.boids['position']
+        V = self.boids['velocity']
 
         # Cohesion: steer to move toward the average position of local
         # flockmates
@@ -158,10 +165,10 @@ class Canvas(app.Canvas):
         R = -((P[I] - Z) * M).sum(axis=1)
 
         # Target : Follow target
-        T = target['position'] - P
+        T = self.target['position'] - P
 
         # Predator : Move away from predator
-        dP = P - predator['position']
+        dP = P - self.predator['position']
         D = np.maximum(0, 0.3 -
                        np.sqrt(dP[:, 0] ** 2 +
                                dP[:, 1] ** 2 +
@@ -169,17 +176,17 @@ class Canvas(app.Canvas):
         D = np.repeat(D, 3, axis=0).reshape(n, 3)
         dP *= D
 
-        #boids['velocity'] += 0.0005*C + 0.01*A + 0.01*R + 0.0005*T + 0.0025*dP
-        boids['velocity'] += 0.0005 * C + 0.01 * \
+        #self.boids['velocity'] += 0.0005*C + 0.01*A + 0.01*R +
+        #                           0.0005*T + 0.0025*dP
+        self.boids['velocity'] += 0.0005 * C + 0.01 * \
             A + 0.01 * R + 0.0005 * T + 0.025 * dP
-        boids['position'] += boids['velocity']
+        self.boids['position'] += self.boids['velocity']
 
-        self.vbo_position.set_data(particles['position'])
+        self.vbo_position.set_data(self.particles['position'].copy())
 
         return t
 
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/brain.py b/examples/demo/gloo/brain.py
index eb5859d..a863950 100644
--- a/examples/demo/gloo/brain.py
+++ b/examples/demo/gloo/brain.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 # vispy: gallery 2
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -16,7 +16,7 @@ from vispy import app
 from vispy.util.transforms import perspective, translate, rotate
 from vispy.io import load_data_file
 
-brain = np.load(load_data_file('brain/brain.npz'))
+brain = np.load(load_data_file('brain/brain.npz', force_download='2014-09-04'))
 data = brain['vertex_buffer']
 faces = brain['index_buffer']
 
@@ -82,7 +82,7 @@ void main()
     vec3 surfaceToCamera = vec3(0.0, 0.0, 1.0) - position;
     vec3 K = normalize(normalize(surfaceToLight) + normalize(surfaceToCamera));
     float specular = clamp(pow(abs(dot(normal, K)), 40.), 0.0, 1.0);
-    
+
     gl_FragColor = v_color * brightness * vec4(u_light_intensity, 1);
 }
 """
@@ -94,39 +94,35 @@ class Canvas(app.Canvas):
         self.size = 800, 600
 
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
-        
+
         self.theta, self.phi = -80, 180
         self.translate = 3
 
         self.faces = gloo.IndexBuffer(faces)
         self.program.bind(gloo.VertexBuffer(data))
-        
+
         self.program['u_color'] = 1, 1, 1, 1
         self.program['u_light_position'] = (1., 1., 1.)
         self.program['u_light_intensity'] = (1., 1., 1.)
-        
+
+        self.apply_zoom()
+
+        gloo.set_state(blend=False, depth_test=True, polygon_offset_fill=True)
+
         self._t0 = default_timer()
         self._timer = app.Timer('auto', connect=self.on_timer, start=True)
-        
+
         self.update_matrices()
 
     def update_matrices(self):
-        self.view = np.eye(4, dtype=np.float32)
-        self.model = np.eye(4, dtype=np.float32)
+        self.view = translate((0, 0, -self.translate))
+        self.model = np.dot(rotate(self.theta, (1, 0, 0)),
+                            rotate(self.phi, (0, 1, 0)))
         self.projection = np.eye(4, dtype=np.float32)
-        
-        rotate(self.model, self.theta, 1, 0, 0)
-        rotate(self.model, self.phi, 0, 1, 0)
-        
-        translate(self.view, 0, 0, -self.translate)
-        
         self.program['u_model'] = self.model
         self.program['u_view'] = self.view
-        self.program['u_normal'] = np.array(np.matrix(np.dot(self.view, 
-                                                             self.model)).I.T)
-        
-    def on_initialize(self, event):
-        gloo.set_state(blend=False, depth_test=True, polygon_offset_fill=True)
+        self.program['u_normal'] = np.linalg.inv(np.dot(self.view,
+                                                        self.model)).T
 
     def on_timer(self, event):
         elapsed = default_timer() - self._t0
@@ -135,10 +131,7 @@ class Canvas(app.Canvas):
         self.update()
 
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = perspective(45.0, width / float(height), 1.0, 20.0)
-        self.program['u_projection'] = self.projection
+        self.apply_zoom()
 
     def on_mouse_wheel(self, event):
         self.translate += -event.delta[1]/5.
@@ -150,6 +143,12 @@ class Canvas(app.Canvas):
         gloo.clear()
         self.program.draw('triangles', indices=self.faces)
 
+    def apply_zoom(self):
+        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])
+        self.projection = perspective(45.0, self.size[0] /
+                                      float(self.size[1]), 1.0, 20.0)
+        self.program['u_projection'] = self.projection
+
 if __name__ == '__main__':
     c = Canvas()
     c.show()
diff --git a/examples/demo/gloo/camera.py b/examples/demo/gloo/camera.py
index a86b231..9c07a29 100644
--- a/examples/demo/gloo/camera.py
+++ b/examples/demo/gloo/camera.py
@@ -1,8 +1,9 @@
-#!/usr/bin/env python
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
+# -----------------------------------------------------------------------------
 """Display a live webcam feed. Require OpenCV (Python 2 only).
 """
 
@@ -10,7 +11,7 @@ try:
     import cv2
 except Exception:
     raise ImportError("You need OpenCV for this example.")
-    
+
 import numpy as np
 from vispy import app
 from vispy import gloo
@@ -32,7 +33,7 @@ fragment = """
     void main()
     {
         gl_FragColor = texture2D(texture, v_texcoord);
-        
+
         // HACK: the image is in BGR instead of RGB.
         float temp = gl_FragColor.r;
         gl_FragColor.r = gl_FragColor.b;
@@ -48,13 +49,19 @@ class Canvas(app.Canvas):
         self.program['position'] = [(-1, -1), (-1, +1), (+1, -1), (+1, +1)]
         self.program['texcoord'] = [(1, 1), (1, 0), (0, 1), (0, 0)]
         self.program['texture'] = np.zeros((480, 640, 3)).astype(np.uint8)
+
+        width, height = self.physical_size
+        gloo.set_viewport(0, 0, width, height)
+
         self.cap = cv2.VideoCapture(0)
         if not self.cap.isOpened():
             raise Exception("There's no available camera.")
         self._timer = app.Timer('auto', connect=self.on_timer, start=True)
 
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
+        width, height = event.physical_size
         gloo.set_viewport(0, 0, width, height)
 
     def on_draw(self, event):
@@ -67,6 +74,5 @@ class Canvas(app.Canvas):
         self.update()
         
 c = Canvas()
-c.show()
 app.run()
 c.cap.release()
diff --git a/examples/demo/gloo/cloud.py b/examples/demo/gloo/cloud.py
index 97e0d06..b48cbea 100644
--- a/examples/demo/gloo/cloud.py
+++ b/examples/demo/gloo/cloud.py
@@ -12,22 +12,6 @@ from vispy import gloo
 from vispy import app
 from vispy.util.transforms import perspective, translate, rotate
 
-
-# Create vertices
-n = 1000000
-data = np.zeros(n, [('a_position', np.float32, 3),
-                    ('a_bg_color', np.float32, 4),
-                    ('a_fg_color', np.float32, 4),
-                    ('a_size', np.float32, 1)])
-data['a_position'] = 0.45 * np.random.randn(n, 3)
-data['a_bg_color'] = np.random.uniform(0.85, 1.00, (n, 4))
-data['a_fg_color'] = 0, 0, 0, 1
-data['a_size'] = np.random.uniform(5, 10, n)
-u_linewidth = 1.0
-u_antialias = 1.0
-u_size = 1
-
-
 vert = """
 #version 120
 
@@ -231,15 +215,29 @@ void main()
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-        self.size = 800, 600
+        app.Canvas.__init__(self, keys='interactive', size=(800, 600))
+        ps = self.pixel_scale
+
+        # Create vertices
+        n = 1000000
+        data = np.zeros(n, [('a_position', np.float32, 3),
+                            ('a_bg_color', np.float32, 4),
+                            ('a_fg_color', np.float32, 4),
+                            ('a_size', np.float32, 1)])
+        data['a_position'] = 0.45 * np.random.randn(n, 3)
+        data['a_bg_color'] = np.random.uniform(0.85, 1.00, (n, 4))
+        data['a_fg_color'] = 0, 0, 0, 1
+        data['a_size'] = np.random.uniform(5*ps, 10*ps, n)
+        u_linewidth = 1.0
+        u_antialias = 1.0
 
+        self.translate = 5
         self.program = gloo.Program(vert, frag)
-        self.view = np.eye(4, dtype=np.float32)
+        self.view = translate((0, 0, -self.translate))
         self.model = np.eye(4, dtype=np.float32)
         self.projection = np.eye(4, dtype=np.float32)
-        self.translate = 5
-        translate(self.view, 0, 0, -self.translate)
+
+        self.apply_zoom()
 
         self.program.bind(gloo.VertexBuffer(data))
         self.program['u_linewidth'] = u_linewidth
@@ -251,10 +249,11 @@ class Canvas(app.Canvas):
         self.theta = 0
         self.phi = 0
 
+        gloo.set_state('translucent', clear_color='white')
+
         self.timer = app.Timer('auto', connect=self.on_timer, start=True)
 
-    def on_initialize(self, event):
-        gloo.set_state('translucent', clear_color='white')
+        self.show()
 
     def on_key_press(self, event):
         if event.text == ' ':
@@ -266,23 +265,18 @@ class Canvas(app.Canvas):
     def on_timer(self, event):
         self.theta += .5
         self.phi += .5
-        self.model = np.eye(4, dtype=np.float32)
-        rotate(self.model, self.theta, 0, 0, 1)
-        rotate(self.model, self.phi, 0, 1, 0)
+        self.model = np.dot(rotate(self.theta, (0, 0, 1)),
+                            rotate(self.phi, (0, 1, 0)))
         self.program['u_model'] = self.model
         self.update()
 
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = perspective(45.0, width / float(height), 1.0, 1000.0)
-        self.program['u_projection'] = self.projection
+        self.apply_zoom()
 
     def on_mouse_wheel(self, event):
-        self.translate += event.delta[1]
+        self.translate -= event.delta[1]
         self.translate = max(2, self.translate)
-        self.view = np.eye(4, dtype=np.float32)
-        translate(self.view, 0, 0, -self.translate)
+        self.view = translate((0, 0, -self.translate))
 
         self.program['u_view'] = self.view
         self.program['u_size'] = 5 / self.translate
@@ -292,8 +286,13 @@ class Canvas(app.Canvas):
         gloo.clear()
         self.program.draw('points')
 
+    def apply_zoom(self):
+        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])
+        self.projection = perspective(45.0, self.size[0] /
+                                      float(self.size[1]), 1.0, 1000.0)
+        self.program['u_projection'] = self.projection
+
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/donut.py b/examples/demo/gloo/donut.py
index e0d5e64..5af7069 100644
--- a/examples/demo/gloo/donut.py
+++ b/examples/demo/gloo/donut.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
@@ -16,23 +16,6 @@ from vispy import gloo
 from vispy import app
 from vispy.util.transforms import perspective, translate, rotate
 
-# Create vertices
-n, p = 50, 40
-data = np.zeros(p * n, [('a_position', np.float32, 2),
-                        ('a_bg_color', np.float32, 4),
-                        ('a_fg_color', np.float32, 4),
-                        ('a_size',     np.float32, 1)])
-data['a_position'][:, 0] = np.resize(np.linspace(0, 2 * np.pi, n), p * n)
-data['a_position'][:, 1] = np.repeat(np.linspace(0, 2 * np.pi, p), n)
-data['a_bg_color'] = np.random.uniform(0.75, 1.00, (n * p, 4))
-data['a_bg_color'][:, 3] = 1
-data['a_fg_color'] = 0, 0, 0, 1
-data['a_size'] = np.random.uniform(8, 8, n * p)
-u_linewidth = 1.0
-u_antialias = 1.0
-u_size = 1
-
-
 vert = """
 #version 120
 
@@ -119,16 +102,33 @@ void main()
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-        self.size = 800, 800
-        self.title = "D'oh ! A big donut"
+        app.Canvas.__init__(self, keys='interactive', size=(800, 800))
+        ps = self.pixel_scale
+
+        self.title = "D'oh! A big donut"
+
+        # Create vertices
+        n, p = 50, 40
+        data = np.zeros(p * n, [('a_position', np.float32, 2),
+                                ('a_bg_color', np.float32, 4),
+                                ('a_fg_color', np.float32, 4),
+                                ('a_size',     np.float32, 1)])
+        data['a_position'][:, 0] = np.resize(np.linspace(
+                                             0, 2 * np.pi, n), p * n)
+        data['a_position'][:, 1] = np.repeat(np.linspace(0, 2 * np.pi, p), n)
+        data['a_bg_color'] = np.random.uniform(0.75, 1.00, (n * p, 4))
+        data['a_bg_color'][:, 3] = 1
+        data['a_fg_color'] = 0, 0, 0, 1
+        # data['a_size'] = np.random.uniform(8*ps, 8*ps, n * p)
+        data['a_size'] = 8.0*ps
+        u_linewidth = 1.0*ps
+        u_antialias = 1.0
 
+        self.translate = 5
         self.program = gloo.Program(vert, frag)
-        self.view = np.eye(4, dtype=np.float32)
+        self.view = translate((0, 0, -self.translate))
         self.model = np.eye(4, dtype=np.float32)
         self.projection = np.eye(4, dtype=np.float32)
-        self.translate = 5
-        translate(self.view, 0, 0, -self.translate)
 
         self.program.bind(gloo.VertexBuffer(data))
         self.program['u_linewidth'] = u_linewidth
@@ -137,15 +137,19 @@ class Canvas(app.Canvas):
         self.program['u_view'] = self.view
         self.program['u_size'] = 5 / self.translate
 
+        self.apply_zoom()
+
         self.theta = 0
         self.phi = 0
         self.clock = 0
         self.stop_rotation = False
 
+        gloo.set_state('translucent', clear_color='white')
+        self.program['u_clock'] = 0.0
+
         self._timer = app.Timer('auto', connect=self.on_timer, start=True)
 
-    def on_initialize(self, event):
-        gloo.set_state('translucent', clear_color='white')
+        self.show()
 
     def on_key_press(self, event):
         if event.text == ' ':
@@ -155,25 +159,20 @@ class Canvas(app.Canvas):
         if not self.stop_rotation:
             self.theta += .5
             self.phi += .5
-            self.model = np.eye(4, dtype=np.float32)
-            rotate(self.model, self.theta, 0, 0, 1)
-            rotate(self.model, self.phi, 0, 1, 0)
+            self.model = np.dot(rotate(self.theta, (0, 0, 1)),
+                                rotate(self.phi, (0, 1, 0)))
             self.program['u_model'] = self.model
         self.clock += np.pi / 1000
         self.program['u_clock'] = self.clock
         self.update()
 
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = perspective(45.0, width / float(height), 1.0, 1000.0)
-        self.program['u_projection'] = self.projection
+        self.apply_zoom()
 
     def on_mouse_wheel(self, event):
-        self.translate += event.delta[1]
+        self.translate -= event.delta[1]
         self.translate = max(2, self.translate)
-        self.view = np.eye(4, dtype=np.float32)
-        translate(self.view, 0, 0, -self.translate)
+        self.view = translate((0, 0, -self.translate))
 
         self.program['u_view'] = self.view
         self.program['u_size'] = 5 / self.translate
@@ -183,8 +182,13 @@ class Canvas(app.Canvas):
         gloo.clear()
         self.program.draw('points')
 
+    def apply_zoom(self):
+        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])
+        self.projection = perspective(45.0, self.size[0] /
+                                      float(self.size[1]), 1.0, 1000.0)
+        self.program['u_projection'] = self.projection
+
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/fireworks.py b/examples/demo/gloo/fireworks.py
index a439c34..0888af4 100644
--- a/examples/demo/gloo/fireworks.py
+++ b/examples/demo/gloo/fireworks.py
@@ -14,8 +14,11 @@ calculated, such that each explostion is unique.
 
 import time
 import numpy as np
-from vispy import gloo
-from vispy import app
+from vispy import gloo, app
+
+# import vispy
+# vispy.use('pyside', 'es2')
+
 
 # Create a texture
 radius = 32
@@ -37,7 +40,6 @@ data = np.zeros(N, [('a_lifetime', np.float32, 1),
 
 
 VERT_SHADER = """
-#version 120
 uniform float u_time;
 uniform vec3 u_centerPosition;
 attribute float a_lifetime;
@@ -62,17 +64,17 @@ void main () {
 }
 """
 
+# Deliberately add precision qualifiers to test automatic GLSL code conversion
 FRAG_SHADER = """
-#version 120
-
+precision highp float;
 uniform sampler2D texture1;
 uniform vec4 u_color;
 varying float v_lifetime;
-uniform sampler2D s_texture;
+uniform highp sampler2D s_texture;
 
 void main()
 {
-    vec4 texColor;
+    highp vec4 texColor;
     texColor = texture2D(s_texture, gl_PointCoord);
     gl_FragColor = vec4(u_color) * texColor;
     gl_FragColor.a *= v_lifetime;
@@ -83,8 +85,7 @@ void main()
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-        self.size = 800, 600
+        app.Canvas.__init__(self, keys='interactive', size=(800, 600))
 
         # Create program
         self._program = gloo.Program(VERT_SHADER, FRAG_SHADER)
@@ -93,16 +94,19 @@ class Canvas(app.Canvas):
 
         # Create first explosion
         self._new_explosion()
-        
-        self._timer = app.Timer('auto', connect=self.update, start=True)
-    
-    def on_initialize(self, event):
+
         # Enable blending
         gloo.set_state(blend=True, clear_color='black',
                        blend_func=('src_alpha', 'one'))
 
+        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])
+
+        self._timer = app.Timer('auto', connect=self.update, start=True)
+
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
+        width, height = event.physical_size
         gloo.set_viewport(0, 0, width, height)
 
     def on_draw(self, event):
@@ -141,5 +145,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/galaxy.py b/examples/demo/gloo/galaxy.py
index 49aa495..e13a663 100644
--- a/examples/demo/gloo/galaxy.py
+++ b/examples/demo/gloo/galaxy.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
-# vispy: gallery 2
-# Copyright (c) 2014, Vispy Development Team.
+# vispy: gallery 30
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -44,16 +44,6 @@ def make_arm(n, angle):
 p = 50000
 n = 3 * p
 
-data = np.zeros(n, [('a_position', np.float32, 3),
-                    ('a_size', np.float32, 1),
-                    ('a_dist', np.float32, 1)])
-for i in range(3):
-    P, S, D = make_arm(p, i * 2 * np.pi / 3)
-    data['a_dist'][(i + 0) * p:(i + 1) * p] = D
-    data['a_position'][(i + 0) * p:(i + 1) * p] = P
-    data['a_size'][(i + 0) * p:(i + 1) * p] = S
-
-
 # Very simple colormap
 cmap = np.array([[255, 124, 0], [255, 163, 76],
                  [255, 192, 130], [255, 214, 173],
@@ -124,18 +114,28 @@ void main()
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-        self.size = 800, 600
+        app.Canvas.__init__(self, keys='interactive', size=(800, 600))
+        ps = self.pixel_scale
+
         self.title = "A very fake galaxy [mouse scroll to zoom]"
 
+        data = np.zeros(n, [('a_position', np.float32, 3),
+                        ('a_size', np.float32, 1),
+                        ('a_dist', np.float32, 1)])
+
+        for i in range(3):
+            P, S, D = make_arm(p, i * 2 * np.pi / 3)
+            data['a_dist'][(i + 0) * p:(i + 1) * p] = D
+            data['a_position'][(i + 0) * p:(i + 1) * p] = P
+            data['a_size'][(i + 0) * p:(i + 1) * p] = S*ps
+
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
-        self.view = np.eye(4, dtype=np.float32)
         self.model = np.eye(4, dtype=np.float32)
         self.projection = np.eye(4, dtype=np.float32)
         self.theta, self.phi = 0, 0
 
         self.translate = 5
-        translate(self.view, 0, 0, -self.translate)
+        self.view = translate((0, 0, -self.translate))
 
         self.program.bind(gloo.VertexBuffer(data))
         self.program['u_colormap'] = gloo.Texture2D(cmap)
@@ -143,14 +143,17 @@ class Canvas(app.Canvas):
         self.program['u_model'] = self.model
         self.program['u_view'] = self.view
 
-        self.timer = app.Timer('auto', connect=self.on_timer)
+        self.apply_zoom()
 
-    def on_initialize(self, event):
         gloo.set_state(depth_test=False, blend=True,
                        blend_func=('src_alpha', 'one'), clear_color='black')
+
         # Start the timer upon initialization.
+        self.timer = app.Timer('auto', connect=self.on_timer)
         self.timer.start()
 
+        self.show()
+
     def on_key_press(self, event):
         if event.text == ' ':
             if self.timer.running:
@@ -161,23 +164,18 @@ class Canvas(app.Canvas):
     def on_timer(self, event):
         self.theta += .11
         self.phi += .13
-        self.model = np.eye(4, dtype=np.float32)
-        rotate(self.model, self.theta, 0, 0, 1)
-        rotate(self.model, self.phi, 0, 1, 0)
+        self.model = np.dot(rotate(self.theta, (0, 0, 1)),
+                            rotate(self.phi, (0, 1, 0)))
         self.program['u_model'] = self.model
         self.update()
 
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = perspective(45.0, width / float(height), 1.0, 1000.0)
-        self.program['u_projection'] = self.projection
+        self.apply_zoom()
 
     def on_mouse_wheel(self, event):
-        self.translate += event.delta[1]
+        self.translate -= event.delta[1]
         self.translate = max(2, self.translate)
-        self.view = np.eye(4, dtype=np.float32)
-        translate(self.view, 0, 0, -self.translate)
+        self.view = translate((0, 0, -self.translate))
         self.program['u_view'] = self.view
         self.program['u_size'] = 5 / self.translate
         self.update()
@@ -186,7 +184,12 @@ class Canvas(app.Canvas):
         gloo.clear()
         self.program.draw('points')
 
+    def apply_zoom(self):
+        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])
+        self.projection = perspective(45.0, self.size[0] /
+                                      float(self.size[1]), 1.0, 1000.0)
+        self.program['u_projection'] = self.projection
+
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/galaxy/galaxy.py b/examples/demo/gloo/galaxy/galaxy.py
new file mode 100755
index 0000000..2d39658
--- /dev/null
+++ b/examples/demo/gloo/galaxy/galaxy.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python
+import numpy as np
+import sys
+
+from vispy.util.transforms import perspective
+from vispy.util import transforms
+from vispy import gloo
+from vispy import app
+from vispy import io
+
+import galaxy_specrend
+from galaxy_simulation import Galaxy
+
+VERT_SHADER = """
+#version 120
+uniform mat4  u_model;
+uniform mat4  u_view;
+uniform mat4  u_projection;
+
+//sampler that maps [0, n] -> color according to blackbody law
+uniform sampler1D u_colormap;
+//index to sample the colormap at
+attribute float a_color_index;
+
+//size of the star
+attribute float a_size;
+//type
+//type 0 - stars
+//type 1 - dust
+//type 2 - h2a objects
+//type 3 - h2a objects
+attribute float a_type;
+attribute vec2  a_position;
+//brightness of the star
+attribute float a_brightness;
+
+varying vec3 v_color;
+void main (void)
+{
+    gl_Position = u_projection * u_view * u_model * vec4(a_position, 0.0,1.0);
+
+    //find base color according to physics from our sampler
+    vec3 base_color = texture1D(u_colormap, a_color_index).rgb;
+    //scale it down according to brightness
+    v_color = base_color * a_brightness;
+
+
+    if (a_size > 2.0)
+    {
+        gl_PointSize = a_size;
+    } else {
+        gl_PointSize = 0.0;
+    }
+
+    if (a_type == 2) {
+        v_color *= vec3(2, 1, 1);
+    }
+    else if (a_type == 3) {
+        v_color = vec3(.9);
+    }
+}
+"""
+
+FRAG_SHADER = """
+#version 120
+//star texture
+uniform sampler2D u_texture;
+//predicted color from black body
+varying vec3 v_color;
+
+void main()
+{
+    //amount of intensity from the grayscale star
+    float star_tex_intensity = texture2D(u_texture, gl_PointCoord).r;
+    gl_FragColor = vec4(v_color * star_tex_intensity, 0.8);
+}
+"""
+
+galaxy = Galaxy(10000)
+galaxy.reset(13000, 4000, 0.0004, 0.90, 0.90, 0.5, 200, 300)
+# coldest and hottest temperatures of out galaxy
+t0, t1 = 200.0, 10000.0
+# total number of discrete colors between t0 and t1
+n = 1000
+dt = (t1 - t0) / n
+
+# maps [0, n) -> colors
+# generate a linear interpolation of temperatures
+# then map the temperatures to colors using black body
+# color predictions
+colors = np.zeros(n, dtype=(np.float32, 3))
+for i in range(n):
+    temperature = t0 + i * dt
+    x, y, z = galaxy_specrend.spectrum_to_xyz(galaxy_specrend.bb_spectrum,
+                                              temperature)
+    r, g, b = galaxy_specrend.xyz_to_rgb(galaxy_specrend.SMPTEsystem, x, y, z)
+    r = min((max(r, 0), 1))
+    g = min((max(g, 0), 1))
+    b = min((max(b, 0), 1))
+    colors[i] = galaxy_specrend.norm_rgb(r, g, b)
+
+
+# load the PNG that we use to blend the star with
+# to provide a circular look to each star.
+def load_galaxy_star_image():
+    fname = io.load_data_file('galaxy/star-particle.png')
+    raw_image = io.read_png(fname)
+
+    return raw_image
+
+
+class Canvas(app.Canvas):
+
+    def __init__(self):
+        # setup initial width, height
+        app.Canvas.__init__(self, keys='interactive', size=(800, 600))
+
+        # create a new shader program
+        self.program = gloo.Program(VERT_SHADER, FRAG_SHADER,
+                                    count=len(galaxy))
+
+        # load the star texture
+        self.texture = gloo.Texture2D(load_galaxy_star_image(),
+                                      interpolation='linear')
+        self.program['u_texture'] = self.texture
+
+        # construct the model, view and projection matrices
+        self.view = transforms.translate((0, 0, -5))
+        self.program['u_view'] = self.view
+
+        self.model = np.eye(4, dtype=np.float32)
+        self.program['u_model'] = self.model
+
+        self.program['u_colormap'] = colors
+
+        w, h = self.size
+        self.projection = perspective(45.0, w / float(h), 1.0, 1000.0)
+        self.program['u_projection'] = self.projection
+
+        # start the galaxy to some decent point in the future
+        galaxy.update(100000)
+        data = self.__create_galaxy_vertex_data()
+
+        # setup the VBO once the galaxy vertex data has been setup
+        # bind the VBO for the first time
+        self.data_vbo = gloo.VertexBuffer(data)
+        self.program.bind(self.data_vbo)
+
+        # setup blending
+        gloo.set_state(clear_color=(0.0, 0.0, 0.03, 1.0),
+                       depth_test=False, blend=True,
+                       blend_func=('src_alpha', 'one'))
+
+        self._timer = app.Timer('auto', connect=self.update, start=True)
+
+    def __create_galaxy_vertex_data(self):
+        data = np.zeros(len(galaxy),
+                        dtype=[('a_size', np.float32, 1),
+                               ('a_position', np.float32, 2),
+                               ('a_color_index', np.float32, 1),
+                               ('a_brightness', np.float32, 1),
+                               ('a_type', np.float32, 1)])
+
+        # see shader for parameter explanations
+        pw, ph = self.physical_size
+        data['a_size'] = galaxy['size'] * max(pw / 800.0, ph / 800.0)
+        data['a_position'] = galaxy['position'] / 13000.0
+
+        data['a_color_index'] = (galaxy['temperature'] - t0) / (t1 - t0)
+        data['a_brightness'] = galaxy['brightness']
+        data['a_type'] = galaxy['type']
+
+        return data
+
+    def on_resize(self, event):
+        # setup the new viewport
+        gloo.set_viewport(0, 0, *event.physical_size)
+        # recompute the projection matrix
+        w, h = event.size
+        self.projection = perspective(45.0, w / float(h),
+                                      1.0, 1000.0)
+        self.program['u_projection'] = self.projection
+
+    def on_draw(self, event):
+        # update the galaxy
+        galaxy.update(50000)  # in years !
+
+        # recreate the numpy array that will be sent as the VBO data
+        data = self.__create_galaxy_vertex_data()
+        # update the VBO
+        self.data_vbo.set_data(data)
+        # bind the VBO to the GL context
+        self.program.bind(self.data_vbo)
+
+        # clear the screen and render
+        gloo.clear(color=True, depth=True)
+        self.program.draw('points')
+
+
+if __name__ == '__main__':
+    c = Canvas()
+    c.show()
+
+    if sys.flags.interactive == 0:
+        app.run()
diff --git a/examples/demo/gloo/galaxy/galaxy_simulation.py b/examples/demo/gloo/galaxy/galaxy_simulation.py
new file mode 100644
index 0000000..912ba52
--- /dev/null
+++ b/examples/demo/gloo/galaxy/galaxy_simulation.py
@@ -0,0 +1,235 @@
+# -*- coding: utf-8 -*-
+# vispy: testskip
+
+# -----------------------------------------------------------------------------
+#  A Galaxy Simulator based on the density wave theory
+#  (c) 2012 Ingo Berg
+#
+#  Simulating a Galaxy with the density wave theory
+#  http://beltoforion.de/galaxy/galaxy_en.html
+#
+#  Python version(c) 2014 Nicolas P.Rougier
+# -----------------------------------------------------------------------------
+import math
+import numpy as np
+
+
+class Galaxy(object):
+    """ Galaxy simulation using the density wave theory """
+
+    def __init__(self, n=20000):
+        """ Initialize galaxy """
+
+        # Excentricity of the innermost ellipse
+        self._inner_excentricity = 0.8
+
+        # Excentricity of the outermost ellipse
+        self._outer_excentricity = 1.0
+
+        #  Velovity at the innermost core in km/s
+        self._center_velocity = 30
+
+        # Velocity at the core edge in km/s
+        self._inner_velocity = 200
+
+        # Velocity at the edge of the disk in km/s
+        self._outer_velocity = 300
+
+        # Angular offset per parsec
+        self._angular_offset = 0.019
+
+        # Inner core radius
+        self._core_radius = 6000
+
+        # Galaxy radius
+        self._galaxy_radius = 15000
+
+        # The radius after which all density waves must have circular shape
+        self._distant_radius = 0
+
+        # Distribution of stars
+        self._star_distribution = 0.45
+
+        # Angular velocity of the density waves
+        self._angular_velocity = 0.000001
+
+        # Number of stars
+        self._stars_count = n
+
+        # Number of dust particles
+        self._dust_count = int(self._stars_count * 0.75)
+
+        # Number of H-II regions
+        self._h2_count = 200
+
+        # Particles
+        dtype = [('theta',       np.float32, 1),
+                 ('velocity',    np.float32, 1),
+                 ('angle',       np.float32, 1),
+                 ('m_a',         np.float32, 1),
+                 ('m_b',         np.float32, 1),
+                 ('size',        np.float32, 1),
+                 ('type',        np.float32, 1),
+                 ('temperature', np.float32, 1),
+                 ('brightness',  np.float32, 1),
+                 ('position',    np.float32, 2)]
+        n = self._stars_count + self._dust_count + 2*self._h2_count
+        self._particles = np.zeros(n, dtype=dtype)
+
+        i0 = 0
+        i1 = i0 + self._stars_count
+        self._stars = self._particles[i0:i1]
+        self._stars['size'] = 3.
+        self._stars['type'] = 0
+
+        i0 = i1
+        i1 = i0 + self._dust_count
+        self._dust = self._particles[i0:i1]
+        self._dust['size'] = 64
+        self._dust['type'] = 1
+
+        i0 = i1
+        i1 = i0 + self._h2_count
+        self._h2a = self._particles[i0:i1]
+        self._h2a['size'] = 0
+        self._h2a['type'] = 2
+
+        i0 = i1
+        i1 = i0 + self._h2_count
+        self._h2b = self._particles[i0:i1]
+        self._h2b['size'] = 0
+        self._h2b['type'] = 3
+
+    def __len__(self):
+        """ Number of particles """
+
+        if self._particles is not None:
+            return len(self._particles)
+        return 0
+
+    def __getitem__(self, key):
+        """ x.__getitem__(y) <==> x[y] """
+
+        if self._particles is not None:
+            return self._particles[key]
+        return None
+
+    def reset(self, rad, radCore, deltaAng,
+              ex1, ex2, sigma, velInner, velOuter):
+
+        # Initialize parameters
+        # ---------------------
+        self._inner_excentricity = ex1
+        self._outer_excentricity = ex2
+        self._inner_velocity = velInner
+        self._outer_velocity = velOuter
+        self._angular_offset = deltaAng
+        self._core_radius = radCore
+        self._galaxy_radius = rad
+        self._distant_radius = self._galaxy_radius * 2
+        self.m_sigma = sigma
+
+        # Initialize stars
+        # ----------------
+        stars = self._stars
+        R = np.random.normal(0, sigma, len(stars)) * self._galaxy_radius
+        stars['m_a'] = R
+        stars['angle'] = 90 - R * self._angular_offset
+        stars['theta'] = np.random.uniform(0, 360, len(stars))
+        stars['temperature'] = np.random.uniform(3000, 9000, len(stars))
+        stars['brightness'] = np.random.uniform(0.05, 0.25, len(stars))
+        stars['velocity'] = 0.000005
+
+        for i in range(len(stars)):
+            stars['m_b'][i] = R[i] * self.excentricity(R[i])
+
+        # Initialize dust
+        # ---------------
+        dust = self._dust
+        X = np.random.uniform(0, 2*self._galaxy_radius, len(dust))
+        Y = np.random.uniform(-self._galaxy_radius, self._galaxy_radius,
+                              len(dust))
+        R = np.sqrt(X*X+Y*Y)
+        dust['m_a'] = R
+        dust['angle'] = R * self._angular_offset
+        dust['theta'] = np.random.uniform(0, 360, len(dust))
+        dust['velocity'] = 0.000005
+        dust['temperature'] = 6000 + R/4
+        dust['brightness'] = np.random.uniform(0.01, 0.02)
+        for i in range(len(dust)):
+            dust['m_b'][i] = R[i] * self.excentricity(R[i])
+
+        # Initialise H-II
+        # ---------------
+        h2a, h2b = self._h2a, self._h2b
+        X = np.random.uniform(-self._galaxy_radius, self._galaxy_radius,
+                              len(h2a))
+        Y = np.random.uniform(-self._galaxy_radius, self._galaxy_radius,
+                              len(h2a))
+        R = np.sqrt(X*X+Y*Y)
+
+        h2a['m_a'] = R
+        h2b['m_a'] = R + 1000
+
+        h2a['angle'] = R * self._angular_offset
+        h2b['angle'] = h2a['angle']
+
+        h2a['theta'] = np.random.uniform(0, 360, len(h2a))
+        h2b['theta'] = h2a['theta']
+
+        h2a['velocity'] = 0.000005
+        h2b['velocity'] = 0.000005
+
+        h2a['temperature'] = np.random.uniform(3000, 9000, len(h2a))
+        h2b['temperature'] = h2a['temperature']
+
+        h2a['brightness'] = np.random.uniform(0.005, 0.010, len(h2a))
+        h2b['brightness'] = h2a['brightness']
+
+        for i in range(len(h2a)):
+            h2a['m_b'][i] = R[i] * self.excentricity(R[i])
+        h2b['m_b'] = h2a['m_b']
+
+    def update(self, timestep=100000):
+        """ Update simulation """
+
+        self._particles['theta'] += self._particles['velocity'] * timestep
+
+        P = self._particles
+        a, b = P['m_a'], P['m_b']
+        theta, beta = P['theta'], -P['angle']
+
+        alpha = theta * math.pi / 180.0
+        cos_alpha = np.cos(alpha)
+        sin_alpha = np.sin(alpha)
+        cos_beta = np.cos(beta)
+        sin_beta = np.sin(beta)
+        P['position'][:, 0] = a*cos_alpha*cos_beta - b*sin_alpha*sin_beta
+        P['position'][:, 1] = a*cos_alpha*sin_beta + b*sin_alpha*cos_beta
+
+        D = np.sqrt(((self._h2a['position'] -
+                    self._h2b['position'])**2).sum(axis=1))
+        S = np.maximum(1, ((1000-D)/10) - 50)
+        self._h2a['size'] = 2.0*S
+        self._h2b['size'] = S/6.0
+
+    def excentricity(self, r):
+
+        # Core region of the galaxy. Innermost part is round
+        # excentricity increasing linear to the border of the core.
+        if r < self._core_radius:
+            return 1 + (r / self._core_radius) * (self._inner_excentricity-1)
+
+        elif r > self._core_radius and r <= self._galaxy_radius:
+            a = self._galaxy_radius - self._core_radius
+            b = self._outer_excentricity - self._inner_excentricity
+            return self._inner_excentricity + (r - self._core_radius) / a * b
+
+        # Excentricity is slowly reduced to 1.
+        elif r > self._galaxy_radius and r < self._distant_radius:
+            a = self._distant_radius - self._galaxy_radius
+            b = 1 - self._outer_excentricity
+            return self._outer_excentricity + (r - self._galaxy_radius) / a * b
+
+        else:
+            return 1
diff --git a/examples/demo/gloo/galaxy/galaxy_specrend.py b/examples/demo/gloo/galaxy/galaxy_specrend.py
new file mode 100644
index 0000000..b042fc5
--- /dev/null
+++ b/examples/demo/gloo/galaxy/galaxy_specrend.py
@@ -0,0 +1,422 @@
+# -*- coding: utf-8 -*-
+# vispy: testskip
+
+"""
+Colour Rendering of Spectra
+
+by John Walker
+http://www.fourmilab.ch/
+
+Last updated: March 9, 2003
+
+Converted to Python by Andrew Hutchins, sometime in early
+2011.
+
+    This program is in the public domain.
+    The modifications are also public domain. (AH)
+
+For complete information about the techniques employed in
+this program, see the World-Wide Web document:
+
+    http://www.fourmilab.ch/documents/specrend/
+
+The xyz_to_rgb() function, which was wrong in the original
+version of this program, was corrected by:
+
+    Andrew J. S. Hamilton 21 May 1999
+    Andrew.Hamilton at Colorado.EDU
+    http://casa.colorado.edu/~ajsh/
+
+who also added the gamma correction facilities and
+modified constrain_rgb() to work by desaturating the
+colour by adding white.
+
+A program which uses these functions to plot CIE
+"tongue" diagrams called "ppmcie" is included in
+the Netpbm graphics toolkit:
+
+    http://netpbm.sourceforge.net/
+
+(The program was called cietoppm in earlier
+versions of Netpbm.)
+
+"""
+import math
+
+# A colour system is defined by the CIE x and y coordinates of
+# its three primary illuminants and the x and y coordinates of
+# the white point.
+
+GAMMA_REC709 = 0
+
+NTSCsystem = {"name": "NTSC",
+              "xRed": 0.67, "yRed": 0.33,
+              "xGreen": 0.21, "yGreen": 0.71,
+              "xBlue": 0.14, "yBlue": 0.08,
+              "xWhite": 0.3101, "yWhite": 0.3163, "gamma": GAMMA_REC709}
+
+EBUsystem = {"name": "SUBU (PAL/SECAM)",
+             "xRed": 0.64, "yRed": 0.33,
+             "xGreen": 0.29, "yGreen": 0.60,
+             "xBlue": 0.15, "yBlue": 0.06,
+             "xWhite": 0.3127, "yWhite": 0.3291, "gamma": GAMMA_REC709}
+SMPTEsystem = {"name": "SMPTE",
+               "xRed": 0.63, "yRed": 0.34,
+               "xGreen": 0.31, "yGreen": 0.595,
+               "xBlue": 0.155, "yBlue": 0.07,
+               "xWhite": 0.3127, "yWhite": 0.3291, "gamma": GAMMA_REC709}
+
+HDTVsystem = {"name": "HDTV",
+              "xRed": 0.67, "yRed": 0.33,
+              "xGreen": 0.21, "yGreen": 0.71,
+              "xBlue": 0.15, "yBlue": 0.06,
+              "xWhite": 0.3127, "yWhite": 0.3291, "gamma": GAMMA_REC709}
+
+CIEsystem = {"name": "CIE",
+             "xRed": 0.7355, "yRed": 0.2645,
+             "xGreen": 0.2658, "yGreen": 0.7243,
+             "xBlue": 0.1669, "yBlue": 0.0085,
+             "xWhite": 0.3333333333, "yWhite": 0.3333333333,
+             "gamma": GAMMA_REC709}
+
+Rec709system = {"name": "CIE REC709",
+                "xRed": 0.64, "yRed": 0.33,
+                "xGreen": 0.30, "yGreen": 0.60,
+                "xBlue": 0.15, "yBlue": 0.06,
+                "xWhite": 0.3127, "yWhite": 0.3291,
+                "gamma": GAMMA_REC709}
+
+
+def upvp_to_xy(up, vp):
+    xc = (9 * up) / ((6 * up) - (16 * vp) + 12)
+    yc = (4 * vp) / ((6 * up) - (16 * vp) + 12)
+    return(xc, yc)
+
+
+def xy_toupvp(xc, yc):
+    up = (4 * xc) / ((-2 * xc) + (12 * yc) + 3)
+    vp = (9 * yc) / ((-2 * xc) + (12 * yc) + 3)
+    return(up, vp)
+
+
+def xyz_to_rgb(cs, xc, yc, zc):
+    """
+    Given an additive tricolour system CS, defined by the CIE x
+    and y chromaticities of its three primaries (z is derived
+    trivially as 1-(x+y)), and a desired chromaticity (XC, YC,
+    ZC) in CIE space, determine the contribution of each
+    primary in a linear combination which sums to the desired
+    chromaticity.  If the  requested chromaticity falls outside
+    the Maxwell  triangle (colour gamut) formed by the three
+    primaries, one of the r, g, or b weights will be negative.
+
+    Caller can use constrain_rgb() to desaturate an
+    outside-gamut colour to the closest representation within
+    the available gamut and/or norm_rgb to normalise the RGB
+    components so the largest nonzero component has value 1.
+    """
+
+    xr = cs["xRed"]
+    yr = cs["yRed"]
+    zr = 1 - (xr + yr)
+    xg = cs["xGreen"]
+    yg = cs["yGreen"]
+    zg = 1 - (xg + yg)
+    xb = cs["xBlue"]
+    yb = cs["yBlue"]
+    zb = 1 - (xb + yb)
+    xw = cs["xWhite"]
+    yw = cs["yWhite"]
+    zw = 1 - (xw + yw)
+
+    rx = (yg * zb) - (yb * zg)
+    ry = (xb * zg) - (xg * zb)
+    rz = (xg * yb) - (xb * yg)
+    gx = (yb * zr) - (yr * zb)
+    gy = (xr * zb) - (xb * zr)
+    gz = (xb * yr) - (xr * yb)
+    bx = (yr * zg) - (yg * zr)
+    by = (xg * zr) - (xr * zg)
+    bz = (xr * yg) - (xg * yr)
+
+    rw = ((rx * xw) + (ry * yw) + (rz * zw)) / yw
+    gw = ((gx * xw) + (gy * yw) + (gz * zw)) / yw
+    bw = ((bx * xw) + (by * yw) + (bz * zw)) / yw
+
+    rx = rx / rw
+    ry = ry / rw
+    rz = rz / rw
+    gx = gx / gw
+    gy = gy / gw
+    gz = gz / gw
+    bx = bx / bw
+    by = by / bw
+    bz = bz / bw
+
+    r = (rx * xc) + (ry * yc) + (rz * zc)
+    g = (gx * xc) + (gy * yc) + (gz * zc)
+    b = (bx * xc) + (by * yc) + (bz * zc)
+
+    return(r, g, b)
+
+
+def inside_gamut(r, g, b):
+    """
+     Test whether a requested colour is within the gamut
+     achievable with the primaries of the current colour
+     system.  This amounts simply to testing whether all the
+     primary weights are non-negative. */
+    """
+    return (r >= 0) and (g >= 0) and (b >= 0)
+
+
+def constrain_rgb(r, g, b):
+    """
+    If the requested RGB shade contains a negative weight for
+    one of the primaries, it lies outside the colour gamut
+    accessible from the given triple of primaries.  Desaturate
+    it by adding white, equal quantities of R, G, and B, enough
+    to make RGB all positive.  The function returns 1 if the
+    components were modified, zero otherwise.
+    """
+    # Amount of white needed is w = - min(0, *r, *g, *b)
+    w = -min([0, r, g, b])  # I think?
+
+    # Add just enough white to make r, g, b all positive.
+    if w > 0:
+        r += w
+        g += w
+        b += w
+    return(r, g, b)
+
+
+def gamma_correct(cs, c):
+    """
+    Transform linear RGB values to nonlinear RGB values. Rec.
+    709 is ITU-R Recommendation BT. 709 (1990) ``Basic
+    Parameter Values for the HDTV Standard for the Studio and
+    for International Programme Exchange'', formerly CCIR Rec.
+    709. For details see
+
+       http://www.poynton.com/ColorFAQ.html
+       http://www.poynton.com/GammaFAQ.html
+    """
+    gamma = cs.gamma
+
+    if gamma == GAMMA_REC709:
+        cc = 0.018
+        if c < cc:
+            c = ((1.099 * math.pow(cc, 0.45)) - 0.099) / cc
+        else:
+            c = (1.099 * math.pow(c, 0.45)) - 0.099
+    else:
+        c = math.pow(c, 1.0 / gamma)
+    return(c)
+
+
+def gamma_correct_rgb(cs, r, g, b):
+    r = gamma_correct(cs, r)
+    g = gamma_correct(cs, g)
+    b = gamma_correct(cs, b)
+    return (r, g, b)
+
+
+def norm_rgb(r, g, b):
+    """
+    Normalise RGB components so the most intense (unless all
+    are zero) has a value of 1.
+    """
+    greatest = max([r, g, b])
+
+    if greatest > 0:
+        r /= greatest
+        g /= greatest
+        b /= greatest
+    return(r, g, b)
+
+
+# spec_intens is a function
+def spectrum_to_xyz(spec_intens, temp):
+    """
+    Calculate the CIE X, Y, and Z coordinates corresponding to
+    a light source with spectral distribution given by  the
+    function SPEC_INTENS, which is called with a series of
+    wavelengths between 380 and 780 nm (the argument is
+    expressed in meters), which returns emittance at  that
+    wavelength in arbitrary units.  The chromaticity
+    coordinates of the spectrum are returned in the x, y, and z
+    arguments which respect the identity:
+
+        x + y + z = 1.
+
+    CIE colour matching functions xBar, yBar, and zBar for
+    wavelengths from 380 through 780 nanometers, every 5
+    nanometers.  For a wavelength lambda in this range::
+
+        cie_colour_match[(lambda - 380) / 5][0] = xBar
+        cie_colour_match[(lambda - 380) / 5][1] = yBar
+        cie_colour_match[(lambda - 380) / 5][2] = zBar
+
+    AH Note 2011: This next bit is kind of irrelevant on modern
+    hardware. Unless you are desperate for speed.
+    In which case don't use the Python version!
+
+    To save memory, this table can be declared as floats
+    rather than doubles; (IEEE) float has enough
+    significant bits to represent the values. It's declared
+    as a double here to avoid warnings about "conversion
+    between floating-point types" from certain persnickety
+    compilers.
+    """
+
+    cie_colour_match = [
+        [0.0014, 0.0000, 0.0065],
+        [0.0022, 0.0001, 0.0105],
+        [0.0042, 0.0001, 0.0201],
+        [0.0076, 0.0002, 0.0362],
+        [0.0143, 0.0004, 0.0679],
+        [0.0232, 0.0006, 0.1102],
+        [0.0435, 0.0012, 0.2074],
+        [0.0776, 0.0022, 0.3713],
+        [0.1344, 0.0040, 0.6456],
+        [0.2148, 0.0073, 1.0391],
+        [0.2839, 0.0116, 1.3856],
+        [0.3285, 0.0168, 1.6230],
+        [0.3483, 0.0230, 1.7471],
+        [0.3481, 0.0298, 1.7826],
+        [0.3362, 0.0380, 1.7721],
+        [0.3187, 0.0480, 1.7441],
+        [0.2908, 0.0600, 1.6692],
+        [0.2511, 0.0739, 1.5281],
+        [0.1954, 0.0910, 1.2876],
+        [0.1421, 0.1126, 1.0419],
+        [0.0956, 0.1390, 0.8130],
+        [0.0580, 0.1693, 0.6162],
+        [0.0320, 0.2080, 0.4652],
+        [0.0147, 0.2586, 0.3533],
+        [0.0049, 0.3230, 0.2720],
+        [0.0024, 0.4073, 0.2123],
+        [0.0093, 0.5030, 0.1582],
+        [0.0291, 0.6082, 0.1117],
+        [0.0633, 0.7100, 0.0782],
+        [0.1096, 0.7932, 0.0573],
+        [0.1655, 0.8620, 0.0422],
+        [0.2257, 0.9149, 0.0298],
+        [0.2904, 0.9540, 0.0203],
+        [0.3597, 0.9803, 0.0134],
+        [0.4334, 0.9950, 0.0087],
+        [0.5121, 1.0000, 0.0057],
+        [0.5945, 0.9950, 0.0039],
+        [0.6784, 0.9786, 0.0027],
+        [0.7621, 0.9520, 0.0021],
+        [0.8425, 0.9154, 0.0018],
+        [0.9163, 0.8700, 0.0017],
+        [0.9786, 0.8163, 0.0014],
+        [1.0263, 0.7570, 0.0011],
+        [1.0567, 0.6949, 0.0010],
+        [1.0622, 0.6310, 0.0008],
+        [1.0456, 0.5668, 0.0006],
+        [1.0026, 0.5030, 0.0003],
+        [0.9384, 0.4412, 0.0002],
+        [0.8544, 0.3810, 0.0002],
+        [0.7514, 0.3210, 0.0001],
+        [0.6424, 0.2650, 0.0000],
+        [0.5419, 0.2170, 0.0000],
+        [0.4479, 0.1750, 0.0000],
+        [0.3608, 0.1382, 0.0000],
+        [0.2835, 0.1070, 0.0000],
+        [0.2187, 0.0816, 0.0000],
+        [0.1649, 0.0610, 0.0000],
+        [0.1212, 0.0446, 0.0000],
+        [0.0874, 0.0320, 0.0000],
+        [0.0636, 0.0232, 0.0000],
+        [0.0468, 0.0170, 0.0000],
+        [0.0329, 0.0119, 0.0000],
+        [0.0227, 0.0082, 0.0000],
+        [0.0158, 0.0057, 0.0000],
+        [0.0114, 0.0041, 0.0000],
+        [0.0081, 0.0029, 0.0000],
+        [0.0058, 0.0021, 0.0000],
+        [0.0041, 0.0015, 0.0000],
+        [0.0029, 0.0010, 0.0000],
+        [0.0020, 0.0007, 0.0000],
+        [0.0014, 0.0005, 0.0000],
+        [0.0010, 0.0004, 0.0000],
+        [0.0007, 0.0002, 0.0000],
+        [0.0005, 0.0002, 0.0000],
+        [0.0003, 0.0001, 0.0000],
+        [0.0002, 0.0001, 0.0000],
+        [0.0002, 0.0001, 0.0000],
+        [0.0001, 0.0000, 0.0000],
+        [0.0001, 0.0000, 0.0000],
+        [0.0001, 0.0000, 0.0000],
+        [0.0000, 0.0000, 0.0000]]
+
+    X = 0
+    Y = 0
+    Z = 0
+    # lambda = 380; lambda < 780.1; i++, lambda += 5) {
+    for i, lamb in enumerate(range(380, 780, 5)):
+        Me = spec_intens(lamb, temp)
+        X += Me * cie_colour_match[i][0]
+        Y += Me * cie_colour_match[i][1]
+        Z += Me * cie_colour_match[i][2]
+    XYZ = (X + Y + Z)
+    x = X / XYZ
+    y = Y / XYZ
+    z = Z / XYZ
+
+    return(x, y, z)
+
+
+def bb_spectrum(wavelength, bbTemp=5000):
+    """
+    Calculate, by Planck's radiation law, the emittance of a black body
+    of temperature bbTemp at the given wavelength (in metres).  */
+    """
+    wlm = wavelength * 1e-9  # Convert to metres
+    return (3.74183e-16 *
+            math.pow(wlm, -5.0)) / (math.exp(1.4388e-2 / (wlm * bbTemp)) - 1.0)
+
+    """  Built-in test program which displays the x, y, and Z and RGB
+    values for black body spectra from 1000 to 10000 degrees kelvin.
+    When run, this program should produce the following output:
+
+    Temperature       x      y      z       R     G     B
+    -----------    ------ ------ ------   ----- ----- -----
+       1000 K      0.6528 0.3444 0.0028   1.000 0.007 0.000 (Approximation)
+       1500 K      0.5857 0.3931 0.0212   1.000 0.126 0.000 (Approximation)
+       2000 K      0.5267 0.4133 0.0600   1.000 0.234 0.010
+       2500 K      0.4770 0.4137 0.1093   1.000 0.349 0.067
+       3000 K      0.4369 0.4041 0.1590   1.000 0.454 0.151
+       3500 K      0.4053 0.3907 0.2040   1.000 0.549 0.254
+       4000 K      0.3805 0.3768 0.2428   1.000 0.635 0.370
+       4500 K      0.3608 0.3636 0.2756   1.000 0.710 0.493
+       5000 K      0.3451 0.3516 0.3032   1.000 0.778 0.620
+       5500 K      0.3325 0.3411 0.3265   1.000 0.837 0.746
+       6000 K      0.3221 0.3318 0.3461   1.000 0.890 0.869
+       6500 K      0.3135 0.3237 0.3628   1.000 0.937 0.988
+       7000 K      0.3064 0.3166 0.3770   0.907 0.888 1.000
+       7500 K      0.3004 0.3103 0.3893   0.827 0.839 1.000
+       8000 K      0.2952 0.3048 0.4000   0.762 0.800 1.000
+       8500 K      0.2908 0.3000 0.4093   0.711 0.766 1.000
+       9000 K      0.2869 0.2956 0.4174   0.668 0.738 1.000
+       9500 K      0.2836 0.2918 0.4246   0.632 0.714 1.000
+      10000 K      0.2807 0.2884 0.4310   0.602 0.693 1.000
+"""
+
+if __name__ == "__main__":
+    print("Temperature       x      y      z       R     G     B\n")
+    print("-----------    ------ ------ ------   ----- ----- -----\n")
+
+    for t in range(1000, 10000, 500):  # (t = 1000; t <= 10000; t+= 500) {
+        x, y, z = spectrum_to_xyz(bb_spectrum, t)
+
+        r, g, b = xyz_to_rgb(SMPTEsystem, x, y, z)
+
+        print("  %5.0f K      %.4f %.4f %.4f   " % (t, x, y, z))
+
+        # I omit the approximation bit here.
+        r, g, b = constrain_rgb(r, g, b)
+        r, g, b = norm_rgb(r, g, b)
+        print("%.3f %.3f %.3f" % (r, g, b))
diff --git a/examples/demo/gloo/game_of_life.py b/examples/demo/gloo/game_of_life.py
index 92de6d3..ed72e18 100644
--- a/examples/demo/gloo/game_of_life.py
+++ b/examples/demo/gloo/game_of_life.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 200
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author:   Nicolas P .Rougier
@@ -14,8 +14,8 @@ Conway game of life.
 """
 
 import numpy as np
-from vispy.gloo import (Program, FrameBuffer, DepthBuffer, clear, set_viewport,
-                        set_state)
+from vispy.gloo import (Program, FrameBuffer, RenderBuffer,
+                        clear, set_viewport, set_state)
 from vispy import app
 
 
@@ -111,12 +111,10 @@ class Canvas(app.Canvas):
     def __init__(self):
         app.Canvas.__init__(self, title="Conway game of life",
                             size=(512, 512), keys='interactive')
-        self._timer = app.Timer('auto', connect=self.update, start=True)
-    
-    def on_initialize(self, event):
+
         # Build programs
         # --------------
-        self.comp_size = (512, 512)
+        self.comp_size = self.size
         size = self.comp_size + (4,)
         Z = np.zeros(size, dtype=np.float32)
         Z[...] = np.random.randint(0, 2, size)
@@ -156,27 +154,27 @@ class Canvas(app.Canvas):
         self.render['pingpong'] = self.pingpong
 
         self.fbo = FrameBuffer(self.compute["texture"],
-                               DepthBuffer(self.comp_size))
+                               RenderBuffer(self.comp_size))
         set_state(depth_test=False, clear_color='black')
 
+        self._timer = app.Timer('auto', connect=self.update, start=True)
+
+        self.show()
+
     def on_draw(self, event):
         with self.fbo:
             set_viewport(0, 0, *self.comp_size)
             self.compute["texture"].interpolation = 'nearest'
             self.compute.draw('triangle_strip')
         clear()
-        set_viewport(0, 0, *self.size)
+        set_viewport(0, 0, *self.physical_size)
         self.render["texture"].interpolation = 'linear'
         self.render.draw('triangle_strip')
         self.pingpong = 1 - self.pingpong
         self.compute["pingpong"] = self.pingpong
         self.render["pingpong"] = self.pingpong
 
-    def on_reshape(self, event):
-        set_viewport(0, 0, *event.size)
-
 
 if __name__ == '__main__':
     canvas = Canvas()
-    canvas.show()
     app.run()
diff --git a/examples/demo/gloo/glsl_sandbox_cube.py b/examples/demo/gloo/glsl_sandbox_cube.py
index 47bd363..d602faf 100644
--- a/examples/demo/gloo/glsl_sandbox_cube.py
+++ b/examples/demo/gloo/glsl_sandbox_cube.py
@@ -62,8 +62,7 @@ faces_buffer = gloo.IndexBuffer(faces.astype(np.uint16))
 class Canvas(app.Canvas):
 
     def __init__(self, **kwargs):
-        app.Canvas.__init__(self, **kwargs)
-        self.geometry = 0, 0, 400, 400
+        app.Canvas.__init__(self, size=(400, 400), **kwargs)
 
         self.program = gloo.Program(VERT_CODE, FRAG_CODE)
 
@@ -75,45 +74,48 @@ class Canvas(app.Canvas):
 
         # Handle transformations
         self.init_transforms()
-        
-        self._timer = app.Timer('auto', connect=self.update_transforms)
-        self._timer.start()
-    
-    def on_initialize(self, event):
+
+        self.apply_zoom()
+
         gloo.set_clear_color((1, 1, 1, 1))
         gloo.set_state(depth_test=True)
 
+        self._timer = app.Timer('auto', connect=self.update_transforms)
+        self._timer.start()
+
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = perspective(45.0, width / float(height), 2.0, 10.0)
-        self.program['u_projection'] = self.projection
+        self.apply_zoom()
 
     def on_draw(self, event):
         gloo.clear()
         self.program.draw('triangles', faces_buffer)
 
     def init_transforms(self):
-        self.view = np.eye(4, dtype=np.float32)
-        self.model = np.eye(4, dtype=np.float32)
-        self.projection = np.eye(4, dtype=np.float32)
-
         self.theta = 0
         self.phi = 0
+        self.view = translate((0, 0, -5))
+        self.model = np.eye(4, dtype=np.float32)
+        self.projection = np.eye(4, dtype=np.float32)
 
-        translate(self.view, 0, 0, -5)
         self.program['u_model'] = self.model
         self.program['u_view'] = self.view
 
     def update_transforms(self, event):
         self.theta += .5
         self.phi += .5
-        self.model = np.eye(4, dtype=np.float32)
-        rotate(self.model, self.theta, 0, 0, 1)
-        rotate(self.model, self.phi, 0, 1, 0)
+        self.model = np.dot(rotate(self.theta, (0, 0, 1)),
+                            rotate(self.phi, (0, 1, 0)))
         self.program['u_model'] = self.model
         self.update()
 
+    def apply_zoom(self):
+        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])
+        self.projection = perspective(45.0, self.size[0] /
+                                      float(self.size[1]), 2.0, 10.0)
+        self.program['u_projection'] = self.projection
+
 
 class TextField(QtGui.QPlainTextEdit):
 
@@ -146,9 +148,7 @@ class MainWindow(QtGui.QWidget):
         self.fragEdit.setPlainText(FRAG_CODE)
 
         # Create a canvas
-        self.canvas = Canvas()
-        self.canvas.create_native()
-        self.canvas.native.setParent(self)
+        self.canvas = Canvas(parent=self)
 
         # Layout
         hlayout = QtGui.QHBoxLayout(self)
@@ -164,20 +164,17 @@ class MainWindow(QtGui.QWidget):
         vlayout.addWidget(self.fragEdit, 1)
         vlayout.addWidget(self.theButton, 0)
 
+        self.show()
+
     def on_compile(self):
         vert_code = str(self.vertEdit.toPlainText())
         frag_code = str(self.fragEdit.toPlainText())
-        self.canvas.program.shaders[0].code = vert_code
-        self.canvas.program.shaders[1].code = frag_code
-        # Because the code has changed, the variables are re-created,
-        # so we need to reset them. This can be considered a bug in gloo
-        # and should be addressed at some point.
-        self.canvas.program['u_projection'] = self.canvas.projection
-        self.canvas.program['u_view'] = self.canvas.view
-        
+        self.canvas.program.set_shaders(vert_code, frag_code)
+        # Note how we do not need to reset our variables, they are
+        # re-set automatically (by gloo)
+
 
 if __name__ == '__main__':
     app.create()
     m = MainWindow()
-    m.show()
     app.run()
diff --git a/examples/demo/gloo/graph.py b/examples/demo/gloo/graph.py
index b216d78..9d9f405 100644
--- a/examples/demo/gloo/graph.py
+++ b/examples/demo/gloo/graph.py
@@ -124,32 +124,35 @@ void main(){
 }
 """
 
-n = 100
-ne = 100
-data = np.zeros(n, dtype=[('a_position', np.float32, 3),
-                          ('a_fg_color', np.float32, 4),
-                          ('a_bg_color', np.float32, 4),
-                          ('a_size', np.float32, 1),
-                          ('a_linewidth', np.float32, 1),
-                          ])
-edges = np.random.randint(size=(ne, 2), low=0, high=n).astype(np.uint32)
-data['a_position'] = np.hstack((.25 * np.random.randn(n, 2), np.zeros((n, 1))))
-data['a_fg_color'] = 0, 0, 0, 1
-color = np.random.uniform(0.5, 1., (n, 3))
-data['a_bg_color'] = np.hstack((color, np.ones((n, 1))))
-data['a_size'] = np.random.randint(size=n, low=10, high=30)
-data['a_linewidth'] = 2
-u_antialias = 1
-
 
 class Canvas(app.Canvas):
 
     def __init__(self, **kwargs):
         # Initialize the canvas for real
-        app.Canvas.__init__(self, keys='interactive', **kwargs)
-        self.size = 512, 512
+        app.Canvas.__init__(self, keys='interactive', size=(512, 512),
+                            **kwargs)
+        ps = self.pixel_scale
         self.position = 50, 50
 
+        n = 100
+        ne = 100
+        data = np.zeros(n, dtype=[('a_position', np.float32, 3),
+                                  ('a_fg_color', np.float32, 4),
+                                  ('a_bg_color', np.float32, 4),
+                                  ('a_size', np.float32, 1),
+                                  ('a_linewidth', np.float32, 1),
+                                  ])
+        edges = np.random.randint(size=(ne, 2), low=0,
+                                  high=n).astype(np.uint32)
+        data['a_position'] = np.hstack((.25 * np.random.randn(n, 2),
+                                       np.zeros((n, 1))))
+        data['a_fg_color'] = 0, 0, 0, 1
+        color = np.random.uniform(0.5, 1., (n, 3))
+        data['a_bg_color'] = np.hstack((color, np.ones((n, 1))))
+        data['a_size'] = np.random.randint(size=n, low=8*ps, high=20*ps)
+        data['a_linewidth'] = 1.*ps
+        u_antialias = 1
+
         self.vbo = gloo.VertexBuffer(data)
         self.index = gloo.IndexBuffer(edges)
         self.view = np.eye(4, dtype=np.float32)
@@ -164,16 +167,18 @@ class Canvas(app.Canvas):
         self.program['u_view'] = self.view
         self.program['u_projection'] = self.projection
 
+        set_viewport(0, 0, *self.physical_size)
+
         self.program_e = gloo.Program(vs, fs)
         self.program_e.bind(self.vbo)
 
-    def on_initialize(self, event):
         set_state(clear_color='white', depth_test=False, blend=True,
                   blend_func=('src_alpha', 'one_minus_src_alpha'))
 
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
-        set_viewport(0, 0, width, height)
+        set_viewport(0, 0, *event.physical_size)
 
     def on_draw(self, event):
         clear(color=True, depth=True)
@@ -182,5 +187,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     c = Canvas(title="Graph")
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/grayscott.py b/examples/demo/gloo/grayscott.py
index 1127403..05980a5 100644
--- a/examples/demo/gloo/grayscott.py
+++ b/examples/demo/gloo/grayscott.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 2000
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author:   Nicolas P .Rougier
@@ -11,7 +11,7 @@
 # -----------------------------------------------------------------------------
 
 import numpy as np
-from vispy.gloo import (Program, FrameBuffer, DepthBuffer, set_viewport,
+from vispy.gloo import (Program, FrameBuffer, RenderBuffer, set_viewport,
                         clear, set_state)
 from vispy import app
 
@@ -126,11 +126,9 @@ class Canvas(app.Canvas):
     def __init__(self):
         app.Canvas.__init__(self, title='Grayscott Reaction-Diffusion',
                             size=(512, 512), keys='interactive')
-        self._timer = app.Timer('auto', connect=self.update, start=True)
-    
-    def on_initialize(self, event):
+
         self.scale = 4
-        self.comp_size = (256, 256)
+        self.comp_size = self.size
         comp_w, comp_h = self.comp_size
         dt = 1.0
         dd = 1.5
@@ -181,16 +179,20 @@ class Canvas(app.Canvas):
         self.render['pingpong'] = self.pingpong
 
         self.fbo = FrameBuffer(self.compute["texture"],
-                               DepthBuffer(self.comp_size))
+                               RenderBuffer(self.comp_size))
         set_state(depth_test=False, clear_color='black')
 
+        self._timer = app.Timer('auto', connect=self.update, start=True)
+
+        self.show()
+
     def on_draw(self, event):
         with self.fbo:
             set_viewport(0, 0, *self.comp_size)
             self.compute["texture"].interpolation = 'nearest'
             self.compute.draw('triangle_strip')
         clear(color=True)
-        set_viewport(0, 0, *self.size)
+        set_viewport(0, 0, *self.physical_size)
         self.render["texture"].interpolation = 'linear'
         self.render.draw('triangle_strip')
         self.pingpong = 1 - self.pingpong
@@ -198,10 +200,9 @@ class Canvas(app.Canvas):
         self.render["pingpong"] = self.pingpong
 
     def on_resize(self, event):
-        set_viewport(0, 0, *self.size)
+        set_viewport(0, 0, *self.physical_size)
 
 
 if __name__ == '__main__':
     canvas = Canvas()
-    canvas.show()
     app.run()
diff --git a/examples/demo/gloo/high_frequency.py b/examples/demo/gloo/high_frequency.py
new file mode 100644
index 0000000..1853349
--- /dev/null
+++ b/examples/demo/gloo/high_frequency.py
@@ -0,0 +1,119 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: gallery 20
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier, Guillaume Bâty. All Rights Reserved.
+# Distributed under the (new) BSD License.
+# -----------------------------------------------------------------------------
+# High frequency (below pixel resolution) function plot
+#
+#  -> http://blog.hvidtfeldts.net/index.php/2011/07/plotting-high-frequency-fun
+#     ctions-using-a-gpu/
+#  -> https://www.shadertoy.com/view/4sB3zz
+# -----------------------------------------------------------------------------
+from vispy import gloo, app, keys
+
+VERT_SHADER = """
+attribute vec2 a_position;
+void main (void)
+{
+    gl_Position = vec4(a_position, 0.0, 1.0);
+}
+"""
+
+FRAG_SHADER = """
+uniform vec2 u_resolution;
+uniform float u_global_time;
+
+// --- Your function here ---
+float function( float x )
+{
+    float d = 3.0 - 2.0*(1.0+cos(u_global_time/5.0))/2.0;
+    return sin(pow(x,d))*sin(x);
+}
+// --- Your function here ---
+
+
+float sample(vec2 uv)
+{
+    const int samples = 128;
+    const float fsamples = float(samples);
+    vec2 maxdist = vec2(0.5,1.0)/40.0;
+    vec2 halfmaxdist = vec2(0.5) * maxdist;
+
+    float stepsize = maxdist.x / fsamples;
+    float initial_offset_x = -0.5 * fsamples * stepsize;
+    uv.x += initial_offset_x;
+    float hit = 0.0;
+    for( int i=0; i<samples; ++i )
+    {
+        float x = uv.x + stepsize * float(i);
+        float y = uv.y;
+        float fx = function(x);
+        float dist = abs(y-fx);
+        hit += step(dist, halfmaxdist.y);
+    }
+    const float arbitraryFactor = 4.5;
+    const float arbitraryExp = 0.95;
+    return arbitraryFactor * pow( hit / fsamples, arbitraryExp );
+}
+
+void main(void)
+{
+    vec2 uv = gl_FragCoord.xy / u_resolution.xy;
+    float ymin = -2.0;
+    float ymax = +2.0;
+    float xmin = 0.0;
+    float xmax = xmin + (ymax-ymin)* u_resolution.x / u_resolution.y;
+
+    vec2 xy = vec2(xmin,ymin) + uv*vec2(xmax-xmin, ymax-ymin);
+    gl_FragColor = vec4(0.0,0.0,0.0, sample(xy));
+}
+"""
+
+
+class Canvas(app.Canvas):
+    def __init__(self, pause=False):
+        app.Canvas.__init__(self, size=(800, 600), keys='interactive')
+        self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
+        self.program["u_global_time"] = 0
+        self.program['a_position'] = [(-1, -1), (-1, +1),
+                                      (+1, -1), (+1, +1)]
+
+        self.apply_zoom()
+
+        gloo.set_state(blend=True,
+                       blend_func=('src_alpha', 'one_minus_src_alpha'))
+
+        self._timer = app.Timer('auto', connect=self.on_timer_event,
+                                start=True)
+
+        self.show()
+
+    def on_resize(self, event):
+        self.apply_zoom()
+
+    def on_draw(self, event):
+        gloo.clear('white')
+        self.program.draw(mode='triangle_strip')
+
+    def on_timer_event(self, event):
+        if self._timer.running:
+            self.program["u_global_time"] += event.dt
+        self.update()
+
+    def on_key_press(self, event):
+        if event.key is keys.SPACE:
+            if self._timer.running:
+                self._timer.stop()
+            else:
+                self._timer.start()
+
+    def apply_zoom(self):
+        self.program["u_resolution"] = self.physical_size
+        gloo.set_viewport(0, 0, *self.physical_size)
+
+
+if __name__ == '__main__':
+    c = Canvas()
+    app.run()
diff --git a/examples/demo/gloo/imshow.py b/examples/demo/gloo/imshow.py
index 0601f36..97f2d3d 100644
--- a/examples/demo/gloo/imshow.py
+++ b/examples/demo/gloo/imshow.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
-# vispy: gallery 1
+# vispy: gallery 10
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
@@ -71,8 +71,6 @@ uniform float vmax;
 uniform float cmap;
 
 uniform sampler2D image;
-uniform vec2 image_shape;
-
 uniform sampler2D colormaps;
 uniform vec2 colormaps_shape;
 
@@ -97,6 +95,8 @@ void main()
 
 class Canvas(app.Canvas):
     def __init__(self):
+        app.Canvas.__init__(self, size=(512, 512),
+                            keys='interactive')
         self.image = Program(img_vertex, img_fragment, 4)
         self.image['position'] = (-1, -1), (-1, +1), (+1, -1), (+1, +1)
         self.image['texcoord'] = (0, 0), (0, +1), (+1, 0), (+1, +1)
@@ -108,18 +108,16 @@ class Canvas(app.Canvas):
         self.image['colormaps'].interpolation = 'linear'
         self.image['colormaps_shape'] = colormaps.shape[1], colormaps.shape[0]
 
-        self.image['image'] = I
+        self.image['image'] = I.astype('float32')
         self.image['image'].interpolation = 'linear'
-        self.image['image_shape'] = I.shape[1], I.shape[0]
-        app.Canvas.__init__(self, show=True, size=(512, 512),
-                            keys='interactive')
 
-    def on_initialize(self, event):
         set_clear_color('black')
 
+        self.show()
+
     def on_resize(self, event):
-        width, height = event.size
-        set_viewport(0, 0, *event.size)
+        width, height = event.physical_size
+        set_viewport(0, 0, *event.physical_size)
 
     def on_draw(self, event):
         clear(color=True, depth=True)
diff --git a/examples/demo/gloo/imshow_cuts.py b/examples/demo/gloo/imshow_cuts.py
index 893914b..b4b42d0 100644
--- a/examples/demo/gloo/imshow_cuts.py
+++ b/examples/demo/gloo/imshow_cuts.py
@@ -111,6 +111,8 @@ void main()
 
 class Canvas(app.Canvas):
     def __init__(self):
+        app.Canvas.__init__(self, size=(512, 512),
+                            keys='interactive')
 
         self.image = Program(image_vertex, image_fragment, 4)
         self.image['position'] = (-1, -1), (-1, +1), (+1, -1), (+1, +1)
@@ -120,26 +122,27 @@ class Canvas(app.Canvas):
         self.image['cmap'] = 0  # Colormap index to use
         self.image['colormaps'] = colormaps
         self.image['n_colormaps'] = colormaps.shape[0]
-        self.image['image'] = I
+        self.image['image'] = I.astype('float32')
         self.image['image'].interpolation = 'linear'
 
-        self.lines = Program(lines_vertex, lines_fragment, 4+4+514+514)
-        self.lines["position"] = np.zeros((4+4+514+514, 2))
-        color = np.zeros((4+4+514+514, 4))
+        set_viewport(0, 0, *self.physical_size)
+
+        self.lines = Program(lines_vertex, lines_fragment)
+        self.lines["position"] = np.zeros((4+4+514+514, 2), np.float32)
+        color = np.zeros((4+4+514+514, 4), np.float32)
         color[1:1+2, 3] = 0.25
         color[5:5+2, 3] = 0.25
         color[9:9+512, 3] = 0.5
         color[523:523+512, 3] = 0.5
         self.lines["color"] = color
-        app.Canvas.__init__(self, show=True, size=(512, 512),
-                            keys='interactive')
 
-    def on_initialize(self, event):
         set_state(clear_color='white', blend=True,
                   blend_func=('src_alpha', 'one_minus_src_alpha'))
 
+        self.show()
+
     def on_resize(self, event):
-        set_viewport(0, 0, *event.size)
+        set_viewport(0, 0, *event.physical_size)
 
     def on_draw(self, event):
         clear(color=True, depth=True)
@@ -149,8 +152,17 @@ class Canvas(app.Canvas):
     def on_mouse_move(self, event):
         x, y = event.pos
         w, h = self.size
+
+        # Make sure the mouse isn't outside of the viewport.
+        x = max(0, min(x, w - 1))
+        y = max(0, min(y, h - 1))
+
         yf = 1 - y/(h/2.)
         xf = x/(w/2.) - 1
+
+        x_norm = (x*512)//w
+        y_norm = (y*512)//h
+
         P = np.zeros((4+4+514+514, 2), np.float32)
 
         x_baseline = P[:4]
@@ -162,11 +174,11 @@ class Canvas(app.Canvas):
         y_baseline[...] = (xf, -1), (xf, -1), (xf, 1), (xf, 1)
 
         x_profile[1:-1, 0] = np.linspace(-1, 1, 512)
-        x_profile[1:-1, 1] = yf+0.15*I[y]
+        x_profile[1:-1, 1] = yf+0.15*I[y_norm, :]
         x_profile[0] = x_profile[1]
         x_profile[-1] = x_profile[-2]
 
-        y_profile[1:-1, 0] = xf+0.15*I[:, x]
+        y_profile[1:-1, 0] = xf+0.15*I[:, x_norm]
         y_profile[1:-1, 1] = np.linspace(-1, 1, 512)
         y_profile[0] = y_profile[1]
         y_profile[-1] = y_profile[-2]
diff --git a/examples/demo/gloo/jfa/jfa_translation.py b/examples/demo/gloo/jfa/jfa_translation.py
index 86bf539..faf3e33 100644
--- a/examples/demo/gloo/jfa/jfa_translation.py
+++ b/examples/demo/gloo/jfa/jfa_translation.py
@@ -1,4 +1,9 @@
 # -*- coding: utf-8 -*-
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
 """
 Demo of jump flooding algoritm for EDT using GLSL
 Author: Stefan Gustavson (stefan.gustavson at gmail.com)
diff --git a/examples/demo/gloo/jfa/jfa_vispy.py b/examples/demo/gloo/jfa/jfa_vispy.py
index b9b7c0c..fc3b200 100644
--- a/examples/demo/gloo/jfa/jfa_vispy.py
+++ b/examples/demo/gloo/jfa/jfa_vispy.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
+# vispy: testskip - because this example sets inactive attributes on Travis
 
 """
 Demo of jump flooding algoritm for EDT using GLSL
@@ -13,11 +14,12 @@ This version is a vispy-ized translation of jfa_translate.py.
 
 import numpy as np
 from os import path as op
-from PIL import Image
+import sys
+
 from vispy import app
-from vispy.gloo import (Program, VertexShader, FragmentShader, FrameBuffer,
-                        VertexBuffer, Texture2D, set_viewport)
-from vispy.io import load_data_file
+from vispy.gloo import (Program, FrameBuffer, VertexBuffer, Texture2D,
+                        set_viewport)
+from vispy.io import load_data_file, imread
 
 this_dir = op.abspath(op.dirname(__file__))
 
@@ -26,37 +28,15 @@ class Canvas(app.Canvas):
     def __init__(self):
         self.use_shaders = True
         app.Canvas.__init__(self, size=(512, 512), keys='interactive')
-        self._timer = app.Timer('auto', self.update, start=True)
-
-    def _setup_textures(self, fname):
-        img = Image.open(load_data_file('jfa/' + fname))
-        self.texture_size = tuple(img.size)
-        data = np.array(img, np.ubyte)[::-1].copy()
-        self.orig_tex = Texture2D(data, format='luminance')
-        self.orig_tex.wrapping = 'repeat'
-        self.orig_tex.interpolation = 'nearest'
-
-        self.comp_texs = []
-        data = np.zeros(self.texture_size + (4,), np.float32)
-        for _ in range(2):
-            tex = Texture2D(data, format='rgba')
-            tex.interpolation = 'nearest'
-            tex.wrapping = 'clamp_to_edge'
-            self.comp_texs.append(tex)
-        self.fbo_to[0].color_buffer = self.comp_texs[0]
-        self.fbo_to[1].color_buffer = self.comp_texs[1]
-        for program in self.programs:
-            program['texw'], program['texh'] = self.texture_size
-
-    def on_initialize(self, event):
+        # Note: read as bytes, then decode; py2.6 compat
         with open(op.join(this_dir, 'vertex_vispy.glsl'), 'rb') as fid:
-            vert = VertexShader(fid.read().decode('ASCII'))
+            vert = fid.read().decode('ASCII')
         with open(op.join(this_dir, 'fragment_seed.glsl'), 'rb') as f:
-            frag_seed = FragmentShader(f.read().decode('ASCII'))
+            frag_seed = f.read().decode('ASCII')
         with open(op.join(this_dir, 'fragment_flood.glsl'), 'rb') as f:
-            frag_flood = FragmentShader(f.read().decode('ASCII'))
+            frag_flood = f.read().decode('ASCII')
         with open(op.join(this_dir, 'fragment_display.glsl'), 'rb') as f:
-            frag_display = FragmentShader(f.read().decode('ASCII'))
+            frag_display = f.read().decode('ASCII')
         self.programs = [Program(vert, frag_seed),
                          Program(vert, frag_flood),
                          Program(vert, frag_display)]
@@ -70,8 +50,28 @@ class Canvas(app.Canvas):
         vertices['texcoord'] = [[0., 0.], [0., 1.], [1., 0.], [1., 1.]]
         vertices = VertexBuffer(vertices)
         for program in self.programs:
-            program['step'] = 0
             program.bind(vertices)
+        self._timer = app.Timer('auto', self.update, start=True)
+
+        self.show()
+
+    def _setup_textures(self, fname):
+        data = imread(load_data_file('jfa/' + fname))[::-1].copy()
+        if data.ndim == 3:
+            data = data[:, :, 0]  # Travis gets 2, I get three?
+        self.texture_size = data.shape[:2]
+        self.orig_tex = Texture2D(data, format='luminance', wrapping='repeat',
+                                  interpolation='nearest')
+        self.comp_texs = []
+        data = np.zeros(self.texture_size + (4,), np.float32)
+        for _ in range(2):
+            tex = Texture2D(data, format='rgba', wrapping='clamp_to_edge',
+                            interpolation='nearest')
+            self.comp_texs.append(tex)
+        self.fbo_to[0].color_buffer = self.comp_texs[0]
+        self.fbo_to[1].color_buffer = self.comp_texs[1]
+        for program in self.programs[1:2]:
+            program['texw'], program['texh'] = self.texture_size
 
     def on_draw(self, event):
         if self.use_shaders:
@@ -94,7 +94,7 @@ class Canvas(app.Canvas):
             self.programs[2]['texture'] = self.comp_texs[last_rend]
         else:
             self.programs[2]['texture'] = self.orig_tex
-        set_viewport(0, 0, *self.size)
+        set_viewport(0, 0, *self.physical_size)
         self.programs[2].draw('triangle_strip')
 
     def on_key_press(self, event):
@@ -112,6 +112,6 @@ def fun(x):
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     c.measure_fps(callback=fun)
-    c.app.run()
+    if sys.flags.interactive != 1:
+        c.app.run()
diff --git a/examples/demo/gloo/mandelbrot.py b/examples/demo/gloo/mandelbrot.py
index c34127d..321dc0c 100644
--- a/examples/demo/gloo/mandelbrot.py
+++ b/examples/demo/gloo/mandelbrot.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: John David Reaver
@@ -25,56 +25,47 @@ fragment = """
 uniform vec2 resolution;
 uniform vec2 center;
 uniform float scale;
-uniform int iter;
-
-// Jet color scheme
-vec4 color_scheme(float x) {
-    vec3 a, b;
-    float c;
-    if (x < 0.34) {
-        a = vec3(0, 0, 0.5);
-        b = vec3(0, 0.8, 0.95);
-        c = (x - 0.0) / (0.34 - 0.0);
-    } else if (x < 0.64) {
-        a = vec3(0, 0.8, 0.95);
-        b = vec3(0.85, 1, 0.04);
-        c = (x - 0.34) / (0.64 - 0.34);
-    } else if (x < 0.89) {
-        a = vec3(0.85, 1, 0.04);
-        b = vec3(0.96, 0.7, 0);
-        c = (x - 0.64) / (0.89 - 0.64);
-    } else {
-        a = vec3(0.96, 0.7, 0);
-        b = vec3(0.5, 0, 0);
-        c = (x - 0.89) / (1.0 - 0.89);
-    }
-    return vec4(mix(a, b, c), 1.0);
+
+vec3 hot(float t)
+{
+    return vec3(smoothstep(0.00,0.33,t),
+                smoothstep(0.33,0.66,t),
+                smoothstep(0.66,1.00,t));
 }
 
-void main() {
-    vec2 z, c;
+void main()
+{
+    
+    const int n = 300;
+    const float log_2 = 0.6931471805599453;
+
+    vec2 c;
 
     // Recover coordinates from pixel coordinates
     c.x = (gl_FragCoord.x / resolution.x - 0.5) * scale + center.x;
     c.y = (gl_FragCoord.y / resolution.y - 0.5) * scale + center.y;
 
-    // Main Mandelbrot computation
+    float x, y, d;
     int i;
-    z = c;
-    for(i = 0; i < iter; i++) {
-        float x = (z.x * z.x - z.y * z.y) + c.x;
-        float y = (z.y * z.x + z.x * z.y) + c.y;
-
-        if((x * x + y * y) > 4.0) break;
-        z.x = x;
-        z.y = y;
+    vec2 z = c;
+    for(i = 0; i < n; ++i)
+    {
+        x = (z.x*z.x - z.y*z.y) + c.x;
+        y = (z.y*z.x + z.x*z.y) + c.y;
+        d = x*x + y*y;
+        if (d > 4.0) break;
+        z = vec2(x,y);
+    }
+    if ( i < n ) {
+        float nu = log(log(sqrt(d))/log_2)/log_2;
+        float index = float(i) + 1.0 - nu;
+        float v = pow(index/float(n),0.5);
+        gl_FragColor = vec4(hot(v),1.0);
+    } else {
+        gl_FragColor = vec4(hot(0.0),1.0);
     }
-
-    // Convert iterations to color
-    float color = 1.0 - float(i) / float(iter);
-    gl_FragColor = color_scheme(color);
-
 }
+
 """
 
 
@@ -93,23 +84,26 @@ class Canvas(app.Canvas):
 
         self.scale = self.program["scale"] = 3
         self.center = self.program["center"] = [-0.5, 0]
-        self.iterations = self.program["iter"] = 300
-        self.program['resolution'] = self.size
+        self.apply_zoom()
 
         self.bounds = [-2, 2]
         self.min_scale = 0.00005
         self.max_scale = 4
 
+        gloo.set_clear_color(color='black')
+
         self._timer = app.Timer('auto', connect=self.update, start=True)
 
-    def on_initialize(self, event):
-        gloo.set_clear_color(color='black')
+        self.show()
 
     def on_draw(self, event):
         self.program.draw()
 
     def on_resize(self, event):
-        width, height = event.size
+        self.apply_zoom()
+
+    def apply_zoom(self):
+        width, height = self.physical_size
         gloo.set_viewport(0, 0, width, height)
         self.program['resolution'] = [width, height]
 
@@ -155,7 +149,8 @@ class Canvas(app.Canvas):
         wheels :)
 
         """
-        if event.text == '+':
+
+        if event.text == '+' or event.text == '=':
             self.zoom(0.9)
         elif event.text == '-':
             self.zoom(1/0.9)
@@ -167,7 +162,7 @@ class Canvas(app.Canvas):
         while zooming. mouse_coords should come from MouseEvent.pos.
 
         """
-        if mouse_coords:  # Record the position of the mouse
+        if mouse_coords is not None:  # Record the position of the mouse
             x, y = float(mouse_coords[0]), float(mouse_coords[1])
             x0, y0 = self.pixel_to_coords(x, y)
 
@@ -175,12 +170,12 @@ class Canvas(app.Canvas):
         self.scale = max(min(self.scale, self.max_scale), self.min_scale)
         self.program["scale"] = self.scale
 
-        if mouse_coords:  # Translate so the mouse point is stationary
+        # Translate so the mouse point is stationary
+        if mouse_coords is not None:
             x1, y1 = self.pixel_to_coords(x, y)
             self.translate_center(x1 - x0, y1 - y0)
 
 
 if __name__ == '__main__':
     canvas = Canvas(size=(800, 800), keys='interactive')
-    canvas.show()
     app.run()
diff --git a/examples/demo/gloo/mandelbrot.py b/examples/demo/gloo/mandelbrot_double.py
similarity index 52%
copy from examples/demo/gloo/mandelbrot.py
copy to examples/demo/gloo/mandelbrot_double.py
index c34127d..659bea5 100644
--- a/examples/demo/gloo/mandelbrot.py
+++ b/examples/demo/gloo/mandelbrot_double.py
@@ -1,13 +1,29 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: John David Reaver
-# Date:   04/29/2014
+# Date:   09/12/2014
 # -----------------------------------------------------------------------------
 
+"""
+Example demonstrating the use of emulated double-precision floating point
+numbers. Based off of mandelbrot.py.
+
+The shader program emulates double-precision variables using a vec2 instead of
+single-precision floats. Any function starting with ds_* operates on these
+variables. See http://www.thasler.org/blog/?p=93.
+
+NOTE: Some NVIDIA cards optimize the double-precision code away. Results are
+therefore hardware dependent.
+
+"""
+
+from __future__ import division
+
+import numpy as np
 from vispy import app, gloo
 
 # Shader source code
@@ -22,9 +38,14 @@ void main()
 """
 
 fragment = """
-uniform vec2 resolution;
-uniform vec2 center;
-uniform float scale;
+#pragma optionNV(fastmath off)
+#pragma optionNV(fastprecision off)
+
+uniform vec2 inv_resolution_x;  // Inverse resolutions
+uniform vec2 inv_resolution_y;
+uniform vec2 center_x;
+uniform vec2 center_y;
+uniform vec2 scale;
 uniform int iter;
 
 // Jet color scheme
@@ -51,23 +72,98 @@ vec4 color_scheme(float x) {
     return vec4(mix(a, b, c), 1.0);
 }
 
+vec2 ds_set(float a) {
+    // Create an emulated double by storing first part of float in first half
+    // of vec2
+    vec2 z;
+    z.x = a;
+    z.y = 0.0;
+    return z;
+}
+
+vec2 ds_add (vec2 dsa, vec2 dsb)
+{
+    // Add two emulated doubles. Complexity comes from carry-over.
+    vec2 dsc;
+    float t1, t2, e;
+
+    t1 = dsa.x + dsb.x;
+    e = t1 - dsa.x;
+    t2 = ((dsb.x - e) + (dsa.x - (t1 - e))) + dsa.y + dsb.y;
+
+    dsc.x = t1 + t2;
+    dsc.y = t2 - (dsc.x - t1);
+    return dsc;
+}
+
+vec2 ds_mul (vec2 dsa, vec2 dsb)
+{
+    vec2 dsc;
+    float c11, c21, c2, e, t1, t2;
+    float a1, a2, b1, b2, cona, conb, split = 8193.;
+
+    cona = dsa.x * split;
+    conb = dsb.x * split;
+    a1 = cona - (cona - dsa.x);
+    b1 = conb - (conb - dsb.x);
+    a2 = dsa.x - a1;
+    b2 = dsb.x - b1;
+
+    c11 = dsa.x * dsb.x;
+    c21 = a2 * b2 + (a2 * b1 + (a1 * b2 + (a1 * b1 - c11)));
+
+    c2 = dsa.x * dsb.y + dsa.y * dsb.x;
+
+    t1 = c11 + c2;
+    e = t1 - c11;
+    t2 = dsa.y * dsb.y + ((c2 - e) + (c11 - (t1 - e))) + c21;
+
+    dsc.x = t1 + t2;
+    dsc.y = t2 - (dsc.x - t1);
+
+    return dsc;
+}
+
+// Compare: res = -1 if a < b
+//              = 0 if a == b
+//              = 1 if a > b
+float ds_compare(vec2 dsa, vec2 dsb)
+{
+    if (dsa.x < dsb.x) return -1.;
+    else if (dsa.x == dsb.x) {
+        if (dsa.y < dsb.y) return -1.;
+        else if (dsa.y == dsb.y) return 0.;
+        else return 1.;
+    }
+    else return 1.;
+}
+
 void main() {
-    vec2 z, c;
+    vec2 z_x, z_y, c_x, c_y, x, y, frag_x, frag_y;
+    vec2 four = ds_set(4.0);
+    vec2 point5 = ds_set(0.5);
 
     // Recover coordinates from pixel coordinates
-    c.x = (gl_FragCoord.x / resolution.x - 0.5) * scale + center.x;
-    c.y = (gl_FragCoord.y / resolution.y - 0.5) * scale + center.y;
+    frag_x = ds_set(gl_FragCoord.x);
+    frag_y = ds_set(gl_FragCoord.y);
+
+    c_x = ds_add(ds_mul(frag_x, inv_resolution_x), -point5);
+    c_x = ds_add(ds_mul(c_x, scale), center_x);
+    c_y = ds_add(ds_mul(frag_y, inv_resolution_y), -point5);
+    c_y = ds_add(ds_mul(c_y, scale), center_y);
+
 
     // Main Mandelbrot computation
     int i;
-    z = c;
+    z_x = c_x;
+    z_y = c_y;
     for(i = 0; i < iter; i++) {
-        float x = (z.x * z.x - z.y * z.y) + c.x;
-        float y = (z.y * z.x + z.x * z.y) + c.y;
+        x = ds_add(ds_add(ds_mul(z_x, z_x), -ds_mul(z_y, z_y)), c_x);
+        y = ds_add(ds_add(ds_mul(z_y, z_x), ds_mul(z_x, z_y)), c_y);
 
-        if((x * x + y * y) > 4.0) break;
-        z.x = x;
-        z.y = y;
+        if(ds_compare(ds_add(ds_mul(x, x), ds_mul(y, y)), four) > 0.) break;
+        z_x = x;
+        z_y = y;
     }
 
     // Convert iterations to color
@@ -91,27 +187,33 @@ class Canvas(app.Canvas):
         self.program["position"] = [(-1, -1), (-1, 1), (1, 1),
                                     (-1, -1), (1, 1), (1, -1)]
 
-        self.scale = self.program["scale"] = 3
-        self.center = self.program["center"] = [-0.5, 0]
+        self.scale = 3
+        self.program["scale"] = set_emulated_double(self.scale)
+        self.center = [-0.5, 0]
+        self.bounds = [-2, 2]
+        self.translate_center(0, 0)
         self.iterations = self.program["iter"] = 300
-        self.program['resolution'] = self.size
 
-        self.bounds = [-2, 2]
-        self.min_scale = 0.00005
-        self.max_scale = 4
+        self.apply_zoom()
 
-        self._timer = app.Timer('auto', connect=self.update, start=True)
+        self.min_scale = 1e-12
+        self.max_scale = 4
 
-    def on_initialize(self, event):
         gloo.set_clear_color(color='black')
 
+        self.show()
+
     def on_draw(self, event):
         self.program.draw()
 
     def on_resize(self, event):
-        width, height = event.size
+        self.apply_zoom()
+
+    def apply_zoom(self):
+        width, height = self.physical_size
         gloo.set_viewport(0, 0, width, height)
-        self.program['resolution'] = [width, height]
+        self.program['inv_resolution_x'] = set_emulated_double(1 / width)
+        self.program['inv_resolution_y'] = set_emulated_double(1 / height)
 
     def on_mouse_move(self, event):
         """Pan the view based on the change in mouse position."""
@@ -121,6 +223,7 @@ class Canvas(app.Canvas):
             X0, Y0 = self.pixel_to_coords(float(x0), float(y0))
             X1, Y1 = self.pixel_to_coords(float(x1), float(y1))
             self.translate_center(X1 - X0, Y1 - Y0)
+            self.update()
 
     def translate_center(self, dx, dy):
         """Translates the center point, and keeps it in bounds."""
@@ -129,7 +232,12 @@ class Canvas(app.Canvas):
         center[1] -= dy
         center[0] = min(max(center[0], self.bounds[0]), self.bounds[1])
         center[1] = min(max(center[1], self.bounds[0]), self.bounds[1])
-        self.program["center"] = self.center = center
+        self.center = center
+
+        center_x = set_emulated_double(center[0])
+        center_y = set_emulated_double(center[1])
+        self.program["center_x"] = center_x
+        self.program["center_y"] = center_y
 
     def pixel_to_coords(self, x, y):
         """Convert pixel coordinates to Mandelbrot set coordinates."""
@@ -147,6 +255,7 @@ class Canvas(app.Canvas):
             factor = 1 / 0.9
         for _ in range(int(abs(delta))):
             self.zoom(factor, event.pos)
+        self.update()
 
     def on_key_press(self, event):
         """Use + or - to zoom in and out.
@@ -155,10 +264,11 @@ class Canvas(app.Canvas):
         wheels :)
 
         """
-        if event.text == '+':
+        if event.text == '+' or event.text == '=':
             self.zoom(0.9)
         elif event.text == '-':
             self.zoom(1/0.9)
+        self.update()
 
     def zoom(self, factor, mouse_coords=None):
         """Factors less than zero zoom in, and greater than zero zoom out.
@@ -167,20 +277,26 @@ class Canvas(app.Canvas):
         while zooming. mouse_coords should come from MouseEvent.pos.
 
         """
-        if mouse_coords:  # Record the position of the mouse
+        if mouse_coords is not None:  # Record the position of the mouse
             x, y = float(mouse_coords[0]), float(mouse_coords[1])
             x0, y0 = self.pixel_to_coords(x, y)
 
         self.scale *= factor
         self.scale = max(min(self.scale, self.max_scale), self.min_scale)
-        self.program["scale"] = self.scale
+        self.program["scale"] = set_emulated_double(self.scale)
 
-        if mouse_coords:  # Translate so the mouse point is stationary
+        if mouse_coords is not None:  # Translate so mouse point is stationary
             x1, y1 = self.pixel_to_coords(x, y)
             self.translate_center(x1 - x0, y1 - y0)
 
 
+def set_emulated_double(number):
+    """Emulate a double using two numbers of type float32."""
+    double = np.array([number, 0], dtype=np.float32)  # Cast number to float32
+    double[1] = number - double[0]  # Remainder stored in second half of array
+    return double
+
+
 if __name__ == '__main__':
     canvas = Canvas(size=(800, 800), keys='interactive')
-    canvas.show()
     app.run()
diff --git a/examples/demo/gloo/markers.py b/examples/demo/gloo/markers.py
deleted file mode 100644
index 92ecede..0000000
--- a/examples/demo/gloo/markers.py
+++ /dev/null
@@ -1,232 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-# -----------------------------------------------------------------------------
-"""
-Marker shader definitions. You need to combine marker_frag with one of the
-available marker function (marker_disc, marker_diamond, ...)
-"""
-
-
-vert = """
-#version 120
-
-// Uniforms
-// ------------------------------------
-uniform mat4 u_model;
-uniform mat4 u_view;
-uniform mat4 u_projection;
-uniform float u_antialias;
-uniform float u_size;
-
-// Attributes
-// ------------------------------------
-attribute vec3  a_position;
-attribute vec4  a_fg_color;
-attribute vec4  a_bg_color;
-attribute float a_linewidth;
-attribute float a_size;
-
-// Varyings
-// ------------------------------------
-varying vec4 v_fg_color;
-varying vec4 v_bg_color;
-varying float v_size;
-varying float v_linewidth;
-varying float v_antialias;
-
-void main (void) {
-    v_size = a_size * u_size;
-    v_linewidth = a_linewidth;
-    v_antialias = u_antialias;
-    v_fg_color  = a_fg_color;
-    v_bg_color  = a_bg_color;
-    gl_Position = u_projection * u_view * u_model *
-        vec4(a_position*u_size,1.0);
-    gl_PointSize = v_size + 2*(v_linewidth + 1.5*v_antialias);
-}
-"""
-
-
-frag = """
-#version 120
-
-// Constants
-// ------------------------------------
-
-// Varyings
-// ------------------------------------
-varying vec4 v_fg_color;
-varying vec4 v_bg_color;
-varying float v_size;
-varying float v_linewidth;
-varying float v_antialias;
-
-// Functions
-// ------------------------------------
-float marker(vec2 P, float size);
-
-
-// Main
-// ------------------------------------
-void main()
-{
-    float size = v_size +2*(v_linewidth + 1.5*v_antialias);
-    float t = v_linewidth/2.0-v_antialias;
-
-    // The marker function needs to be linked with this shader
-    float r = marker(gl_PointCoord, size);
-
-    float d = abs(r) - t;
-    if( r > (v_linewidth/2.0+v_antialias))
-    {
-        discard;
-    }
-    else if( d < 0.0 )
-    {
-       gl_FragColor = v_fg_color;
-    }
-    else
-    {
-        float alpha = d/v_antialias;
-        alpha = exp(-alpha*alpha);
-        if (r > 0)
-            gl_FragColor = vec4(v_fg_color.rgb, alpha*v_fg_color.a);
-        else
-            gl_FragColor = mix(v_bg_color, v_fg_color, alpha);
-    }
-}
-"""
-
-
-disc = """
-float marker(vec2 P, float size)
-{
-    float r = length((P.xy - vec2(0.5,0.5))*size);
-    r -= v_size/2;
-    return r;
-}
-"""
-
-
-arrow = """
-float marker(vec2 P, float size)
-{
-    float r1 = abs(P.x -.50)*size + abs(P.y -.5)*size - v_size/2;
-    float r2 = abs(P.x -.25)*size + abs(P.y -.5)*size - v_size/2;
-    float r = max(r1,-r2);
-    return r;
-}
-"""
-
-
-ring = """
-float marker(vec2 P, float size)
-{
-    float r1 = length((P.xy - vec2(0.5,0.5))*size) - v_size/2;
-    float r2 = length((P.xy - vec2(0.5,0.5))*size) - v_size/4;
-    float r = max(r1,-r2);
-    return r;
-}
-"""
-
-
-clobber = """
-float marker(vec2 P, float size)
-{
-    const float PI = 3.14159265358979323846264;
-    const float t1 = -PI/2;
-    const vec2  c1 = 0.2*vec2(cos(t1),sin(t1));
-    const float t2 = t1+2*PI/3;
-    const vec2  c2 = 0.2*vec2(cos(t2),sin(t2));
-    const float t3 = t2+2*PI/3;
-    const vec2  c3 = 0.2*vec2(cos(t3),sin(t3));
-
-    float r1 = length((P.xy- vec2(0.5,0.5) - c1)*size);
-    r1 -= v_size/3;
-    float r2 = length((P.xy- vec2(0.5,0.5) - c2)*size);
-    r2 -= v_size/3;
-    float r3 = length((P.xy- vec2(0.5,0.5) - c3)*size);
-    r3 -= v_size/3;
-    float r = min(min(r1,r2),r3);
-    return r;
-}
-"""
-
-
-square = """
-float marker(vec2 P, float size)
-{
-    float r = max(abs(P.x -.5)*size, abs(P.y -.5)*size);
-    r -= v_size/2;
-    return r;
-}
-"""
-
-
-diamond = """
-float marker(vec2 P, float size)
-{
-    float r = abs(P.x -.5)*size + abs(P.y -.5)*size;
-    r -= v_size/2;
-    return r;
-}
-"""
-
-
-vbar = """
-float marker(vec2 P, float size)
-{
-    float r1 = max(abs(P.x - 0.75)*size, abs(P.x - 0.25)*size);
-    float r3 = max(abs(P.x - 0.50)*size, abs(P.y - 0.50)*size);
-    float r = max(r1,r3);
-    r -= v_size/2;
-    return r;
-}
-"""
-
-
-hbar = """
-float marker(vec2 P, float size)
-{
-    float r2 = max(abs(P.y - 0.75)*size, abs(P.y - 0.25)*size);
-    float r3 = max(abs(P.x - 0.50)*size, abs(P.y - 0.50)*size);
-    float r = max(r2,r3);
-    r -= v_size/2;
-    return r;
-}
-"""
-
-
-cross = """
-float marker(vec2 P, float size)
-{
-    float r1 = max(abs(P.x - 0.75)*size, abs(P.x - 0.25)*size);
-    float r2 = max(abs(P.y - 0.75)*size, abs(P.y - 0.25)*size);
-    float r3 = max(abs(P.x - 0.50)*size, abs(P.y - 0.50)*size);
-    float r = max(min(r1,r2),r3);
-    r -= v_size/2;
-    return r;
-}
-"""
-
-tailed_arrow = """
-float marker(vec2 P, float size)
-{
-
-   //arrow_right
-    float r1 = abs(P.x -.50)*size + abs(P.y -.5)*size - v_size/2;
-    float r2 = abs(P.x -.25)*size + abs(P.y -.5)*size - v_size/2;
-    float arrow = max(r1,-r2);
-
-    //hbar
-    float r3 = (abs(P.y-.5)*2+.3)*v_size-v_size/2;
-    float r4 = (P.x -.775)*size;
-    float r6 = abs(P.x -.5)*size-v_size/2;
-    float limit = (P.x -.5)*size + abs(P.y -.5)*size - v_size/2;
-    float hbar = max(limit,max(max(r3,r4),r6));
-
-    return min(arrow,hbar);
-}
-"""
diff --git a/examples/demo/gloo/molecular_viewer.py b/examples/demo/gloo/molecular_viewer.py
index c6a6c7e..0a424b3 100644
--- a/examples/demo/gloo/molecular_viewer.py
+++ b/examples/demo/gloo/molecular_viewer.py
@@ -74,7 +74,7 @@ void main()
     pos.z += v_radius*z;
     vec3 pos2 = pos.xyz;
     pos = u_projection * pos;
-    gl_FragDepth = 0.5*(pos.z / pos.w)+0.5;
+//    gl_FragDepth = 0.5*(pos.z / pos.w)+0.5;
     vec3 normal = vec3(x,y,z);
     float diffuse = clamp(dot(normal, v_light_direction), 0.0, 1.0);
 
@@ -97,15 +97,16 @@ class Canvas(app.Canvas):
 
     def __init__(self):
         app.Canvas.__init__(self, title='Molecular viewer',
-                            keys='interactive')
-        self.size = 1200, 800
+                            keys='interactive', size=(1200, 800))
+        self.ps = self.pixel_scale
 
+        self.translate = 40
         self.program = gloo.Program(vertex, fragment)
-        self.view = np.eye(4, dtype=np.float32)
+        self.view = translate((0, 0, -self.translate))
         self.model = np.eye(4, dtype=np.float32)
         self.projection = np.eye(4, dtype=np.float32)
-        self.translate = 40
-        translate(self.view, 0, 0, -self.translate)
+
+        self.apply_zoom()
 
         fname = load_data_file('molecular_viewer/micelle.npz')
         self.load_molecule(fname)
@@ -114,8 +115,11 @@ class Canvas(app.Canvas):
         self.theta = 0
         self.phi = 0
 
+        gloo.set_state(depth_test=True, clear_color='black')
         self._timer = app.Timer('auto', connect=self.on_timer, start=True)
 
+        self.show()
+
     def load_molecule(self, fname):
         molecule = np.load(fname)['molecule']
         self._nAtoms = molecule.shape[0]
@@ -138,7 +142,7 @@ class Canvas(app.Canvas):
 
         data['a_position'] = self.coords
         data['a_color'] = self.atomsColours
-        data['a_radius'] = self.atomsScales
+        data['a_radius'] = self.atomsScales*self.ps
 
         self.program.bind(gloo.VertexBuffer(data))
 
@@ -147,9 +151,6 @@ class Canvas(app.Canvas):
         self.program['u_light_position'] = 0., 0., 2.
         self.program['u_light_spec_position'] = -5., 5., -5.
 
-    def on_initialize(self, event):
-        gloo.set_state(depth_test=True, clear_color='black')
-
     def on_key_press(self, event):
         if event.text == ' ':
             if self.timer.running:
@@ -162,16 +163,16 @@ class Canvas(app.Canvas):
     def on_timer(self, event):
         self.theta += .25
         self.phi += .25
-        self.model = np.eye(4, dtype=np.float32)
-
-        rotate(self.model, self.theta, 0, 0, 1)
-        rotate(self.model, self.phi, 0, 1, 0)
-
+        self.model = np.dot(rotate(self.theta, (0, 0, 1)),
+                            rotate(self.phi, (0, 1, 0)))
         self.program['u_model'] = self.model
         self.update()
 
     def on_resize(self, event):
         width, height = event.size
+
+    def apply_zoom(self):
+        width, height = self.physical_size
         gloo.set_viewport(0, 0, width, height)
         self.projection = perspective(25.0, width / float(height), 2.0, 100.0)
         self.program['u_projection'] = self.projection
@@ -179,9 +180,7 @@ class Canvas(app.Canvas):
     def on_mouse_wheel(self, event):
         self.translate -= event.delta[1]
         self.translate = max(-1, self.translate)
-        self.view = np.eye(4, dtype=np.float32)
-
-        translate(self.view, 0, 0, -self.translate)
+        self.view = translate((0, 0, -self.translate))
 
         self.program['u_view'] = self.view
         self.update()
@@ -193,5 +192,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     mvc = Canvas()
-    mvc.show()
     app.run()
diff --git a/examples/demo/gloo/ndscatter.py b/examples/demo/gloo/ndscatter.py
index 0e12629..3fe51fc 100644
--- a/examples/demo/gloo/ndscatter.py
+++ b/examples/demo/gloo/ndscatter.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# vispy: gallery 2000
+# vispy: gallery 30
 
 """N-dimensional scatter plot with GPU-based projections.
 The projection axes evolve smoothly over time, following a path on the
@@ -11,38 +11,7 @@ from vispy import app
 from vispy.color import ColorArray
 from vispy.io import load_iris
 import numpy as np
-from scipy.linalg import expm, logm
-
-
-class OrthogonalPath(object):
-    """Implement a continuous path on the Lie group SO(n).
-    
-            >>> op = OrthogonalPath(mat1, mat2)
-            >>> mat = op(t)
-    
-    """
-    def __init__(self, mat, origin=None):
-        if origin is None:
-            origin = np.eye(len(mat))
-        self.a, self.b = np.matrix(origin), np.matrix(mat)
-        self._logainvb = logm(self.a.I * self.b)
-        
-    def __call__(self, t):
-        return np.real(self.a * expm(t * self._logainvb))
-
-# Load the Iris dataset and normalize.
-iris = load_iris()
-position = iris['data'].astype(np.float32)
-n, ndim = position.shape
-position -= position.mean()
-position /= np.abs(position).max()
-v_position = position*.75
-
-v_color = ColorArray(['orange', 'magenta', 'darkblue'])
-v_color = v_color.rgb[iris['group'], :].astype(np.float32)
-v_color *= np.random.uniform(.5, 1.5, (n, 3))
-v_color = np.clip(v_color, 0, 1)
-v_size = np.random.uniform(2, 12, (n, 1)).astype(np.float32)
+from scipy.linalg import logm
 
 VERT_SHADER = """
 #version 120
@@ -67,10 +36,10 @@ void main (void) {
     v_antialias = 1.0;
     v_fg_color  = vec4(0.0,0.0,0.0,0.5);
     v_bg_color  = vec4(a_color,    1.0);
-    
+
     vec2 position = vec2(dot(a_position, u_vec1),
                          dot(a_position, u_vec2));
-    
+
     vec2 position_tr = u_scale * (position + u_pan);
     gl_Position = vec4(position_tr, 0.0, 1.0);
     gl_PointSize = 2.0*(v_radius + v_linewidth + 1.5*v_antialias);
@@ -108,40 +77,61 @@ void main()
 class Canvas(app.Canvas):
     def __init__(self):
         app.Canvas.__init__(self, position=(50, 50), keys='interactive')
+        ps = self.pixel_scale
+
+        # Load the Iris dataset and normalize.
+        iris = load_iris()
+        position = iris['data'].astype(np.float32)
+        n, ndim = position.shape
+        position -= position.mean()
+        position /= np.abs(position).max()
+        v_position = position*.75
+
+        v_color = ColorArray(['orange', 'magenta', 'darkblue'])
+        v_color = v_color.rgb[iris['group'], :].astype(np.float32)
+        v_color *= np.random.uniform(.5, 1.5, (n, 3))
+        v_color = np.clip(v_color, 0, 1)
+        v_size = np.random.uniform(2*ps, 12*ps, (n, 1)).astype(np.float32)
 
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
 
         self.program['a_position'] = gloo.VertexBuffer(v_position)
         self.program['a_color'] = gloo.VertexBuffer(v_color)
         self.program['a_size'] = gloo.VertexBuffer(v_size)
-        
+
         self.program['u_pan'] = (0., 0.)
         self.program['u_scale'] = (1., 1.)
-        
+
         self.program['u_vec1'] = (1., 0., 0., 0.)
         self.program['u_vec2'] = (0., 1., 0., 0.)
-            
+
         # Circulant matrix.
         circ = np.diagflat(np.ones(ndim-1), 1)
         circ[-1, 0] = -1 if ndim % 2 == 0 else 1
-        self._op = OrthogonalPath(np.eye(ndim), circ)
-        
-        self._timer = app.Timer('auto', connect=self.on_timer)
+        self.logcirc = logm(circ)
+        # We will solve the equation dX/dt = log(circ) * X in real time
+        # to compute the matrix exponential expm(t*log(circ)).
+        self.mat = np.eye(ndim)
+        self.dt = .001
+        gloo.set_state(clear_color=(1, 1, 1, 1), blend=True,
+                       blend_func=('src_alpha', 'one_minus_src_alpha'))
+        gloo.set_viewport(0, 0, *self.physical_size)
+
+        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
+        self.show()
 
     def on_timer(self, event):
-        mat = self._op(event.elapsed)
-        self.program['u_vec1'] = mat[:, 0].squeeze()
-        self.program['u_vec2'] = mat[:, 1].squeeze()
+        # We advance the numerical solver from as many dt there have been
+        # since the last update.
+        for t in np.arange(0., event.dt, self.dt):
+            self.mat += self.dt * np.dot(self.logcirc, self.mat).real
+        # We just keep the first two columns of the matrix.
+        self.program['u_vec1'] = self.mat[:, 0].squeeze()
+        self.program['u_vec2'] = self.mat[:, 1].squeeze()
         self.update()
-        
-    def on_initialize(self, event):
-        gloo.set_state(clear_color=(1, 1, 1, 1), blend=True, 
-                       blend_func=('src_alpha', 'one_minus_src_alpha'))
-        self._timer.start()
 
     def on_resize(self, event):
-        self.width, self.height = event.size
-        gloo.set_viewport(0, 0, self.width, self.height)
+        gloo.set_viewport(0, 0, *event.physical_size)
 
     def on_draw(self, event):
         gloo.clear()
@@ -149,5 +139,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/offscreen.py b/examples/demo/gloo/offscreen.py
index 6d1e104..b968921 100644
--- a/examples/demo/gloo/offscreen.py
+++ b/examples/demo/gloo/offscreen.py
@@ -1,6 +1,9 @@
-# !/usr/bin/env python
 # -*- coding: utf-8 -*-
-
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
 """
 Demonstrate how to do offscreen rendering.
 Possible use cases:
@@ -24,7 +27,6 @@ from vispy import gloo
 from vispy import app
 from vispy.util.ptime import time
 from vispy.gloo.util import _screenshot
-import numpy as np
 
 # WARNING: doesn't work with Qt4 (update() does not call on_draw()??)
 app.use_app('glfw')
@@ -96,15 +98,15 @@ void main() {
 
 
 class Canvas(app.Canvas):
-    def __init__(self, size=None):
+    def __init__(self, size=(600, 600)):
         # We hide the canvas upon creation.
         app.Canvas.__init__(self, show=False, size=size)
         self._t0 = time()
         # Texture where we render the scene.
-        self._rendertex = gloo.Texture2D(shape=self.size, dtype=np.float32)
+        self._rendertex = gloo.Texture2D(self.size)
         # FBO.
         self._fbo = gloo.FrameBuffer(self._rendertex,
-                                     gloo.DepthBuffer(self.size))
+                                     gloo.RenderBuffer(self.size))
         # Regular program that will be rendered to the FBO.
         self.program = gloo.Program(vertex, fragment)
         self.program["position"] = [(-1, -1), (-1, 1), (1, 1),
@@ -129,13 +131,14 @@ class Canvas(app.Canvas):
         app.quit()
 
 if __name__ == '__main__':
-    size = (600, 600)
-    c = Canvas(size=size)
+    c = Canvas()
+    size = c.size
     app.run()
+
     # The rendering is done, we get the rendering output (4D NumPy array)
     render = c.im
     print('Finished in %.1fms.' % (c._time*1e3))
-    
+
     # Now, we display this image with matplotlib to check.
     import matplotlib.pyplot as plt
     plt.figure(figsize=(size[0]/100., size[1]/100.), dpi=100)
diff --git a/examples/demo/gloo/primitive_mesh_viewer_qt.py b/examples/demo/gloo/primitive_mesh_viewer_qt.py
new file mode 100644
index 0000000..416b875
--- /dev/null
+++ b/examples/demo/gloo/primitive_mesh_viewer_qt.py
@@ -0,0 +1,378 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+# Abstract: show mesh primitive
+# Keywords: cone, arrow, sphere, cylinder, qt
+# -----------------------------------------------------------------------------
+
+"""
+Test the fps capability of Vispy with meshdata primitive
+"""
+try:
+    from sip import setapi
+    setapi("QVariant", 2)
+    setapi("QString", 2)
+except ImportError:
+    pass
+
+from PyQt4 import QtGui, QtCore
+import sys
+
+import numpy as np
+from vispy import app, gloo
+from vispy.util.transforms import perspective, translate, rotate
+from vispy.geometry import meshdata as md
+from vispy.geometry import generation as gen
+
+OBJECT = {'sphere': [('rows', 3, 1000, 'int', 3),
+                     ('cols', 3, 1000, 'int', 3),
+                     ('radius', 0.1, 10, 'double', 1.0)],
+          'cylinder': [('rows', 4, 1000, 'int', 4),
+                       ('cols', 4, 1000, 'int', 4),
+                       ('radius', 0.1, 10, 'double', 1.0),
+                       ('radius Top.', 0.1, 10, 'double', 1.0),
+                       ('length', 0.1, 10, 'double', 1.0)],
+          'cone': [('cols', 3, 1000, 'int', 3),
+                   ('radius', 0.1, 10, 'double', 1.0),
+                   ('length', 0.1, 10, 'double', 1.0)],
+          'arrow': [('rows', 4, 1000, 'int', 4),
+                    ('cols', 4, 1000, 'int', 4),
+                    ('radius', 0.01, 10, 'double', 0.1),
+                    ('length', 0.1, 10, 'double', 1.0),
+                    ('cone_radius', 0.1, 10, 'double', 0.2),
+                    ('cone_length', 0.0, 10., 'double', 0.3)]}
+
+vert = """
+// Uniforms
+// ------------------------------------
+uniform   mat4 u_model;
+uniform   mat4 u_view;
+uniform   mat4 u_projection;
+uniform   vec4 u_color;
+
+// Attributes
+// ------------------------------------
+attribute vec3 a_position;
+attribute vec3 a_normal;
+attribute vec4 a_color;
+
+// Varying
+// ------------------------------------
+varying vec4 v_color;
+
+void main()
+{
+    v_color = a_color * u_color;
+    gl_Position = u_projection * u_view * u_model * vec4(a_position,1.0);
+}
+"""
+
+
+frag = """
+// Varying
+// ------------------------------------
+varying vec4 v_color;
+
+void main()
+{
+    gl_FragColor = v_color;
+}
+"""
+
+DEFAULT_COLOR = (0, 1, 1, 1)
+# -----------------------------------------------------------------------------
+
+
+class MyMeshData(md.MeshData):
+    """ Add to Meshdata class the capability to export good data for gloo """
+    def __init__(self, vertices=None, faces=None, edges=None,
+                 vertex_colors=None, face_colors=None):
+        md.MeshData.__init__(self, vertices=None, faces=None, edges=None,
+                             vertex_colors=None, face_colors=None)
+
+    def get_glTriangles(self):
+        """
+        Build vertices for a colored mesh.
+            V  is the vertices
+            I1 is the indices for a filled mesh (use with GL_TRIANGLES)
+            I2 is the indices for an outline mesh (use with GL_LINES)
+        """
+        vtype = [('a_position', np.float32, 3),
+                 ('a_normal', np.float32, 3),
+                 ('a_color', np.float32, 4)]
+        vertices = self.get_vertices()
+        normals = self.get_vertex_normals()
+        faces = np.uint32(self.get_faces())
+
+        edges = np.uint32(self.get_edges().reshape((-1)))
+        colors = self.get_vertex_colors()
+
+        nbrVerts = vertices.shape[0]
+        V = np.zeros(nbrVerts, dtype=vtype)
+        V[:]['a_position'] = vertices
+        V[:]['a_normal'] = normals
+        V[:]['a_color'] = colors
+
+        return V, faces.reshape((-1)), edges.reshape((-1))
+# -----------------------------------------------------------------------------
+
+
+class ObjectParam(object):
+    """
+    OBJECT parameter test
+    """
+    def __init__(self, name, list_param):
+        self.name = name
+        self.list_param = list_param
+        self.props = {}
+        self.props['visible'] = True
+        for nameV, minV, maxV, typeV, iniV in list_param:
+            self.props[nameV] = iniV
+# -----------------------------------------------------------------------------
+
+
+class ObjectWidget(QtGui.QWidget):
+    """
+    Widget for editing OBJECT parameters
+    """
+    signal_objet_changed = QtCore.pyqtSignal(ObjectParam, name='objectChanged')
+
+    def __init__(self, parent=None, param=None):
+        super(ObjectWidget, self).__init__(parent)
+
+        if param is None:
+            self.param = ObjectParam('sphere', OBJECT['sphere'])
+        else:
+            self.param = param
+
+        self.gb_c = QtGui.QGroupBox(u"Hide/Show %s" % self.param.name)
+        self.gb_c.setCheckable(True)
+        self.gb_c.setChecked(self.param.props['visible'])
+        self.gb_c.toggled.connect(self.update_param)
+
+        lL = []
+        self.sp = []
+        gb_c_lay = QtGui.QGridLayout()
+        for nameV, minV, maxV, typeV, iniV in self.param.list_param:
+            lL.append(QtGui.QLabel(nameV, self.gb_c))
+            if typeV == 'double':
+                self.sp.append(QtGui.QDoubleSpinBox(self.gb_c))
+                self.sp[-1].setDecimals(2)
+                self.sp[-1].setSingleStep(0.1)
+                self.sp[-1].setLocale(QtCore.QLocale(QtCore.QLocale.English))
+            elif typeV == 'int':
+                self.sp.append(QtGui.QSpinBox(self.gb_c))
+            self.sp[-1].setMinimum(minV)
+            self.sp[-1].setMaximum(maxV)
+            self.sp[-1].setValue(iniV)
+
+        # Layout
+        for pos in range(len(lL)):
+            gb_c_lay.addWidget(lL[pos], pos, 0)
+            gb_c_lay.addWidget(self.sp[pos], pos, 1)
+            # Signal
+            self.sp[pos].valueChanged.connect(self.update_param)
+
+        self.gb_c.setLayout(gb_c_lay)
+
+        vbox = QtGui.QVBoxLayout()
+        hbox = QtGui.QHBoxLayout()
+        hbox.addWidget(self.gb_c)
+        hbox.addStretch(1.0)
+        vbox.addLayout(hbox)
+        vbox.addStretch(1.0)
+
+        self.setLayout(vbox)
+
+    def update_param(self, option):
+        """
+        update param and emit a signal
+        """
+        self.param.props['visible'] = self.gb_c.isChecked()
+        keys = map(lambda x: x[0], self.param.list_param)
+        for pos, nameV in enumerate(keys):
+            self.param.props[nameV] = self.sp[pos].value()
+        # emit signal
+        self.signal_objet_changed.emit(self.param)
+# -----------------------------------------------------------------------------
+
+
+class Canvas(app.Canvas):
+
+    def __init__(self,):
+        app.Canvas.__init__(self)
+        self.size = 800, 600
+        # fovy, zfar params
+        self.fovy = 45.0
+        self.zfar = 10.0
+        width, height = self.size
+        self.aspect = width / float(height)
+
+        self.program = gloo.Program(vert, frag)
+
+        self.model = np.eye(4, dtype=np.float32)
+        self.projection = np.eye(4, dtype=np.float32)
+        self.view = translate((0, 0, -5.0))
+
+        self.program['u_model'] = self.model
+        self.program['u_view'] = self.view
+
+        self.theta = 0
+        self.phi = 0
+        self.visible = True
+
+        self._timer = app.Timer(1.0 / 60)
+        self._timer.connect(self.on_timer)
+        self._timer.start()
+
+    # ---------------------------------
+        gloo.set_clear_color((1, 1, 1, 1))
+        gloo.set_state('opaque')
+        gloo.set_polygon_offset(1, 1)
+
+    # ---------------------------------
+    def on_timer(self, event):
+        self.theta += .5
+        self.phi += .5
+        self.model = np.dot(rotate(self.theta, (0, 0, 1)),
+                            rotate(self.phi, (0, 1, 0)))
+        self.program['u_model'] = self.model
+        self.update()
+
+    # ---------------------------------
+    def on_resize(self, event):
+        width, height = event.size
+        self.size = event.size
+        gloo.set_viewport(0, 0, width, height)
+        self.aspect = width / float(height)
+        self.projection = perspective(self.fovy, width / float(height), 1.0,
+                                      self.zfar)
+        self.program['u_projection'] = self.projection
+
+    # ---------------------------------
+    def on_draw(self, event):
+        gloo.clear()
+        if self.visible:
+            # Filled mesh
+            gloo.set_state(blend=False, depth_test=True,
+                           polygon_offset_fill=True)
+            self.program['u_color'] = 1, 1, 1, 1
+            self.program.draw('triangles', self.filled_buf)
+
+            # Outline
+            gloo.set_state(blend=True, depth_test=True,
+                           polygon_offset_fill=False)
+            gloo.set_depth_mask(False)
+            self.program['u_color'] = 0, 0, 0, 1
+            self.program.draw('lines', self.outline_buf)
+            gloo.set_depth_mask(True)
+
+    # ---------------------------------
+    def set_data(self, vertices, filled, outline):
+        self.filled_buf = gloo.IndexBuffer(filled)
+        self.outline_buf = gloo.IndexBuffer(outline)
+        self.vertices_buff = gloo.VertexBuffer(vertices)
+        self.program.bind(self.vertices_buff)
+        self.update()
+# -----------------------------------------------------------------------------
+
+
+class MainWindow(QtGui.QMainWindow):
+
+    def __init__(self):
+        QtGui.QMainWindow.__init__(self)
+
+        self.resize(700, 500)
+        self.setWindowTitle('vispy example ...')
+
+        self.list_object = QtGui.QListWidget()
+        self.list_object.setAlternatingRowColors(True)
+        self.list_object.itemSelectionChanged.connect(self.list_objectChanged)
+
+        self.list_object.addItems(list(OBJECT.keys()))
+        self.props_widget = ObjectWidget(self)
+        self.props_widget.signal_objet_changed.connect(self.update_view)
+
+        self.splitter_v = QtGui.QSplitter(QtCore.Qt.Vertical)
+        self.splitter_v.addWidget(self.list_object)
+        self.splitter_v.addWidget(self.props_widget)
+
+        self.canvas = Canvas()
+        self.canvas.create_native()
+        self.canvas.native.setParent(self)
+        self.canvas.measure_fps(0.1, self.show_fps)
+
+        # Central Widget
+        splitter1 = QtGui.QSplitter(QtCore.Qt.Horizontal)
+        splitter1.addWidget(self.splitter_v)
+        splitter1.addWidget(self.canvas.native)
+
+        self.setCentralWidget(splitter1)
+
+        # FPS message in statusbar:
+        self.status = self.statusBar()
+        self.status.showMessage("...")
+
+        self.mesh = MyMeshData()
+        self.update_view(self.props_widget.param)
+
+    def list_objectChanged(self):
+        row = self.list_object.currentIndex().row()
+        name = self.list_object.currentIndex().data()
+        if row != -1:
+            self.props_widget.deleteLater()
+            self.props_widget = ObjectWidget(self, param=ObjectParam(name,
+                                             OBJECT[name]))
+            self.splitter_v.addWidget(self.props_widget)
+            self.props_widget.signal_objet_changed.connect(self.update_view)
+            self.update_view(self.props_widget.param)
+
+    def show_fps(self, fps):
+        nbr_tri = self.mesh.n_faces
+        self.status.showMessage("FPS - %.2f and nbr Tri %s " % (fps, nbr_tri))
+
+    def update_view(self, param):
+        cols = param.props['cols']
+        radius = param.props['radius']
+        if param.name == 'sphere':
+            rows = param.props['rows']
+            mesh = gen.create_sphere(cols, rows, radius=radius)
+        elif param.name == 'cone':
+            length = param.props['length']
+            mesh = gen.create_cone(cols, radius=radius, length=length)
+        elif param.name == 'cylinder':
+            rows = param.props['rows']
+            length = param.props['length']
+            radius2 = param.props['radius Top.']
+            mesh = gen.create_cylinder(rows, cols, radius=[radius, radius2],
+                                       length=length)
+        elif param.name == 'arrow':
+            length = param.props['length']
+            rows = param.props['rows']
+            cone_radius = param.props['cone_radius']
+            cone_length = param.props['cone_length']
+            mesh = gen.create_arrow(rows, cols, radius=radius, length=length,
+                                    cone_radius=cone_radius,
+                                    cone_length=cone_length)
+        else:
+            return
+
+        self.canvas.visible = param.props['visible']
+        self.mesh.set_vertices(mesh.get_vertices())
+        self.mesh.set_faces(mesh.get_faces())
+        colors = np.tile(DEFAULT_COLOR, (self.mesh.n_vertices, 1))
+        self.mesh.set_vertex_colors(colors)
+        vertices, filled, outline = self.mesh.get_glTriangles()
+        self.canvas.set_data(vertices, filled, outline)
+
+# Start Qt event loop unless running in interactive mode.
+if __name__ == '__main__':
+
+    appQt = QtGui.QApplication(sys.argv)
+    win = MainWindow()
+    win.show()
+    appQt.exec_()
diff --git a/examples/demo/gloo/quiver.py b/examples/demo/gloo/quiver.py
new file mode 100644
index 0000000..dfc424d
--- /dev/null
+++ b/examples/demo/gloo/quiver.py
@@ -0,0 +1,292 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+# Distributed under the (new) BSD License.
+# -----------------------------------------------------------------------------
+
+from vispy import app, gloo
+
+
+vertex = """
+attribute vec2 position;
+void main()
+{
+    gl_Position = vec4(position, 0.0, 1.0);
+}
+"""
+
+fragment = """
+
+vec4 stroke(float distance, float linewidth, float antialias, vec4 stroke)
+{
+    vec4 frag_color;
+    float t = linewidth/2.0 - antialias;
+    float signed_distance = distance;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/antialias;
+    alpha = exp(-alpha*alpha);
+    if( border_distance > (linewidth/2.0 + antialias) )
+        discard;
+    else if( border_distance < 0.0 )
+        frag_color = stroke;
+    else
+        frag_color = vec4(stroke.rgb, stroke.a * alpha);
+    return frag_color;
+}
+
+vec4 filled(float distance, float linewidth, float antialias, vec4 fill)
+{
+    vec4 frag_color;
+    float t = linewidth/2.0 - antialias;
+    float signed_distance = distance;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/antialias;
+    alpha = exp(-alpha*alpha);
+    // Within linestroke
+    if( border_distance < 0.0 )
+        frag_color = fill;
+    // Within shape
+    else if( signed_distance < 0.0 )
+        frag_color = fill;
+    else
+        // Outside shape
+        if( border_distance > (linewidth/2.0 + antialias) )
+            discard;
+        else // Line stroke exterior border
+            frag_color = vec4(fill.rgb, alpha * fill.a);
+    return frag_color;
+}
+
+// Computes the signed distance from a line
+float line_distance(vec2 p, vec2 p1, vec2 p2) {
+    vec2 center = (p1 + p2) * 0.5;
+    float len = length(p2 - p1);
+    vec2 dir = (p2 - p1) / len;
+    vec2 rel_p = p - center;
+    return dot(rel_p, vec2(dir.y, -dir.x));
+}
+
+// Computes the signed distance from a line segment
+float segment_distance(vec2 p, vec2 p1, vec2 p2) {
+    vec2 center = (p1 + p2) * 0.5;
+    float len = length(p2 - p1);
+    vec2 dir = (p2 - p1) / len;
+    vec2 rel_p = p - center;
+    float dist1 = abs(dot(rel_p, vec2(dir.y, -dir.x)));
+    float dist2 = abs(dot(rel_p, dir)) - 0.5*len;
+    return max(dist1, dist2);
+}
+
+// Computes the center with given radius passing through p1 & p2
+vec4 circle_from_2_points(vec2 p1, vec2 p2, float radius)
+{
+    float q = length(p2-p1);
+    vec2 m = (p1+p2)/2.0;
+    vec2 d = vec2( sqrt(radius*radius - (q*q/4.0)) * (p1.y-p2.y)/q,
+                   sqrt(radius*radius - (q*q/4.0)) * (p2.x-p1.x)/q);
+    return  vec4(m+d, m-d);
+}
+
+float arrow_curved(vec2 texcoord,
+                   float body, float head,
+                   float linewidth, float antialias)
+{
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+    float height = 0.5;
+    vec2 p1 = end - head*vec2(+1.0,+height);
+    vec2 p2 = end - head*vec2(+1.0,-height);
+    vec2 p3 = end;
+    // Head : 3 circles
+    vec2 c1  = circle_from_2_points(p1, p3, 1.25*body).zw;
+    float d1 = length(texcoord - c1) - 1.25*body;
+    vec2 c2  = circle_from_2_points(p2, p3, 1.25*body).xy;
+    float d2 = length(texcoord - c2) - 1.25*body;
+    vec2 c3  = circle_from_2_points(p1, p2, max(body-head, 1.0*body)).xy;
+    float d3 = length(texcoord - c3) - max(body-head, 1.0*body);
+    // Body : 1 segment
+    float d4 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+    // Outside (because of circles)
+    if( texcoord.y > +(2.0*head + antialias) )
+         return 1000.0;
+    if( texcoord.y < -(2.0*head + antialias) )
+         return 1000.0;
+    if( texcoord.x < -(body/2.0 + antialias) )
+         return 1000.0;
+    if( texcoord.x > c1.x ) //(body + antialias) )
+         return 1000.0;
+    return min( d4, -min(d3,min(d1,d2)));
+}
+
+float arrow_triangle(vec2 texcoord,
+                     float body, float head, float height,
+                     float linewidth, float antialias)
+{
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+    // Head : 3 lines
+    float d1 = line_distance(texcoord, end, end - head*vec2(+1.0,-height));
+    float d2 = line_distance(texcoord, end - head*vec2(+1.0,+height), end);
+    float d3 = texcoord.x - end.x + head;
+    // Body : 1 segment
+    float d4 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+    float d = min(max(max(d1, d2), -d3), d4);
+    return d;
+}
+
+float arrow_triangle_90(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_triangle(texcoord, body, head, 1.0, linewidth, antialias);
+}
+
+float arrow_triangle_60(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_triangle(texcoord, body, head, 0.5, linewidth, antialias);
+}
+
+float arrow_triangle_30(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_triangle(texcoord, body, head, 0.25, linewidth, antialias);
+}
+
+float arrow_angle(vec2 texcoord,
+                  float body, float head, float height,
+                  float linewidth, float antialias)
+{
+    float d;
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+    // Arrow tip (beyond segment end)
+    if( texcoord.x > body/2.0) {
+        // Head : 2 segments
+        float d1 = line_distance(texcoord, end, end - head*vec2(+1.0,-height));
+        float d2 = line_distance(texcoord, end - head*vec2(+1.0,+height), end);
+        // Body : 1 segment
+        float d3 = end.x - texcoord.x;
+        d = max(max(d1,d2), d3);
+    } else {
+        // Head : 2 segments
+        float d1 = segment_distance(texcoord,
+                                    end - head*vec2(+1.0,-height), end);
+        float d2 = segment_distance(texcoord,
+                                    end - head*vec2(+1.0,+height), end);
+        // Body : 1 segment
+        float d3 = segment_distance(texcoord, start,
+                                    end - vec2(linewidth,0.0));
+        d = min(min(d1,d2), d3);
+    }
+    return d;
+}
+
+float arrow_angle_90(vec2 texcoord,
+                     float body, float head,
+                     float linewidth, float antialias)
+{
+    return arrow_angle(texcoord, body, head, 1.0, linewidth, antialias);
+}
+
+float arrow_angle_60(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_angle(texcoord, body, head, 0.5, linewidth, antialias);
+}
+
+float arrow_angle_30(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_angle(texcoord, body, head, 0.25, linewidth, antialias);
+}
+
+float arrow_stealth(vec2 texcoord,
+                    float body, float head,
+                    float linewidth, float antialias)
+{
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+    float height = 0.5;
+    // Head : 4 lines
+    float d1 = line_distance(texcoord, end-head*vec2(+1.0,-height),
+                                       end);
+    float d2 = line_distance(texcoord, end-head*vec2(+1.0,-height),
+                                       end-vec2(3.0*head/4.0,0.0));
+    float d3 = line_distance(texcoord, end-head*vec2(+1.0,+height), end);
+    float d4 = line_distance(texcoord, end-head*vec2(+1.0,+0.5),
+                                       end-vec2(3.0*head/4.0,0.0));
+    // Body : 1 segment
+    float d5 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+    return min(d5, max( max(-d1, d3), - max(-d2,d4)));
+}
+
+uniform vec2 iResolution;
+uniform vec2 iMouse;
+void main()
+{
+    const float M_PI = 3.14159265358979323846;
+    const float SQRT_2 = 1.4142135623730951;
+    const float linewidth = 3.0;
+    const float antialias =  1.0;
+    const float rows = 32.0;
+    const float cols = 32.0;
+
+    float body = min(iResolution.x/cols, iResolution.y/rows) / SQRT_2;
+    vec2 texcoord = gl_FragCoord.xy;
+    vec2 size   = iResolution.xy / vec2(cols,rows);
+    vec2 center = (floor(texcoord/size) + vec2(0.5,0.5)) * size;
+    texcoord -= center;
+    float theta = M_PI-atan(center.y-iMouse.y,  center.x-iMouse.x);
+    float cos_theta = cos(theta);
+    float sin_theta = sin(theta);
+
+
+    texcoord = vec2(cos_theta*texcoord.x - sin_theta*texcoord.y,
+                    sin_theta*texcoord.x + cos_theta*texcoord.y);
+
+    float d = arrow_stealth(texcoord, body, 0.25*body, linewidth, antialias);
+    gl_FragColor = filled(d, linewidth, antialias, vec4(0,0,0,1));
+}
+"""
+
+
+canvas = app.Canvas(size=(2*512, 2*512), keys='interactive')
+
+
+ at canvas.connect
+def on_draw(event):
+    gloo.clear('white')
+    program.draw('triangle_strip')
+
+
+ at canvas.connect
+def on_resize(event):
+    program["iResolution"] = event.size
+    gloo.set_viewport(0, 0, event.size[0], event.size[1])
+
+
+ at canvas.connect
+def on_mouse_move(event):
+    x, y = event.pos
+    program["iMouse"] = x, canvas.size[1] - y
+    canvas.update()
+
+
+program = gloo.Program(vertex, fragment, count=4)
+dx, dy = 1, 1
+program['position'] = (-dx, -dy), (-dx, +dy), (+dx, -dy), (+dx, +dy)
+program["iMouse"] = (0., 0.)
+
+if __name__ == '__main__':
+    canvas.show()
+    app.run()
diff --git a/examples/demo/gloo/rain.py b/examples/demo/gloo/rain.py
index a20e6ac..52f0bb6 100755
--- a/examples/demo/gloo/rain.py
+++ b/examples/demo/gloo/rain.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author:   Nicolas P .Rougier
@@ -90,7 +90,6 @@ class Canvas(app.Canvas):
         app.Canvas.__init__(self, title='Rain [Move mouse]',
                             size=(512, 512), keys='interactive')
 
-    def on_initialize(self, event):
         # Build data
         # --------------------------------------
         n = 500
@@ -105,18 +104,27 @@ class Canvas(app.Canvas):
         self.program['u_linewidth'] = 1.00
         self.program['u_model'] = np.eye(4, dtype=np.float32)
         self.program['u_view'] = np.eye(4, dtype=np.float32)
+
+        self.activate_zoom()
+
         gloo.set_clear_color('white')
         gloo.set_state(blend=True,
                        blend_func=('src_alpha', 'one_minus_src_alpha'))
         self.timer = app.Timer('auto', self.on_timer, start=True)
 
+        self.show()
+
     def on_draw(self, event):
         gloo.clear()
         self.program.draw('points')
 
     def on_resize(self, event):
-        gloo.set_viewport(0, 0, *event.size)
-        projection = ortho(0, event.size[0], 0, event.size[1], -1, +1)
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        gloo.set_viewport(0, 0, *self.physical_size)
+        projection = ortho(0, self.size[0], 0,
+                           self.size[1], -1, +1)
         self.program['u_projection'] = projection
 
     def on_timer(self, event):
@@ -127,7 +135,7 @@ class Canvas(app.Canvas):
 
     def on_mouse_move(self, event):
         x, y = event.pos
-        h = gloo.get_parameter('viewport')[3]
+        h = self.size[1]
         self.data['a_position'][self.index] = x, h - y
         self.data['a_size'][self.index] = 5
         self.data['a_fg_color'][self.index] = 0, 0, 0, 1
@@ -136,5 +144,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     canvas = Canvas()
-    canvas.show()
     app.run()
diff --git a/examples/demo/gloo/raytracing.py b/examples/demo/gloo/raytracing.py
index 9e3d0d2..0055789 100644
--- a/examples/demo/gloo/raytracing.py
+++ b/examples/demo/gloo/raytracing.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 300
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
@@ -42,7 +42,6 @@ const int PLANE = 1;
 const int SPHERE_0 = 2;
 const int SPHERE_1 = 3;
 
-uniform float u_time;
 uniform float u_aspect_ratio;
 varying vec2 v_position;
 
@@ -105,7 +104,7 @@ float intersect_plane(vec3 O, vec3 D, vec3 P, vec3 N) {
     return d;
 }
 
-vec3 run(float x, float y, float t) {
+vec3 run(float x, float y) {
     vec3 Q = vec3(x, y, 0.);
     vec3 D = normalize(Q - O);
     int depth = 0;
@@ -115,22 +114,22 @@ vec3 run(float x, float y, float t) {
     vec3 col = vec3(0.0, 0.0, 0.0);
     vec3 col_ray;
     float reflection = 1.;
-    
+
     int object_index;
     vec3 object_color;
     vec3 object_normal;
     float object_reflection;
     vec3 M;
     vec3 N, toL, toO;
-    
+
     while (depth < 5) {
-        
+
         /* start trace_ray */
-        
+
         t_plane = intersect_plane(rayO, rayD, plane_position, plane_normal);
         t0 = intersect_sphere(rayO, rayD, sphere_position_0, sphere_radius_0);
         t1 = intersect_sphere(rayO, rayD, sphere_position_1, sphere_radius_1);
-        
+
         if (t_plane < min(t0, t1)) {
             // Plane.
             M = rayO + rayD * t_plane;
@@ -164,86 +163,91 @@ vec3 run(float x, float y, float t) {
         else {
             break;
         }
-        
+
         N = object_normal;
         toL = normalize(light_position - M);
         toO = normalize(O - M);
-        
+
         // Shadow of the spheres on the plane.
         if (object_index == PLANE) {
-            t0 = intersect_sphere(M + N * .0001, toL, 
+            t0 = intersect_sphere(M + N * .0001, toL,
                                   sphere_position_0, sphere_radius_0);
-            t1 = intersect_sphere(M + N * .0001, toL, 
+            t1 = intersect_sphere(M + N * .0001, toL,
                                   sphere_position_1, sphere_radius_1);
             if (min(t0, t1) < INFINITY) {
                 break;
             }
         }
-        
+
         col_ray = vec3(ambient, ambient, ambient);
         col_ray += light_intensity * max(dot(N, toL), 0.) * object_color;
-        col_ray += light_specular.x * light_color * 
+        col_ray += light_specular.x * light_color *
             pow(max(dot(N, normalize(toL + toO)), 0.), light_specular.y);
-        
+
         /* end trace_ray */
-        
+
         rayO = M + N * .0001;
         rayD = normalize(rayD - 2. * dot(rayD, N) * N);
         col += reflection * col_ray;
         reflection *= object_reflection;
-        
+
         depth++;
     }
-    
+
     return clamp(col, 0., 1.);
 }
 
 void main() {
     vec2 pos = v_position;
-    gl_FragColor = vec4(run(pos.x*u_aspect_ratio, pos.y, u_time), 1.);
+    gl_FragColor = vec4(run(pos.x*u_aspect_ratio, pos.y), 1.);
 }
 """
 
 
 class Canvas(app.Canvas):
     def __init__(self):
-        app.Canvas.__init__(self, position=(300, 100), 
+        app.Canvas.__init__(self, position=(300, 100),
                             size=(800, 600), keys='interactive')
-        
+
         self.program = gloo.Program(vertex, fragment)
         self.program['a_position'] = [(-1., -1.), (-1., +1.),
                                       (+1., -1.), (+1., +1.)]
-
         self.program['sphere_position_0'] = (.75, .1, 1.)
         self.program['sphere_radius_0'] = .6
         self.program['sphere_color_0'] = (0., 0., 1.)
-        
+
         self.program['sphere_position_1'] = (-.75, .1, 2.25)
         self.program['sphere_radius_1'] = .6
         self.program['sphere_color_1'] = (.5, .223, .5)
 
         self.program['plane_position'] = (0., -.5, 0.)
         self.program['plane_normal'] = (0., 1., 0.)
-        
+
         self.program['light_intensity'] = 1.
         self.program['light_specular'] = (1., 50.)
         self.program['light_position'] = (5., 5., -10.)
         self.program['light_color'] = (1., 1., 1.)
         self.program['ambient'] = .05
         self.program['O'] = (0., 0., -1.)
-                                      
+
+        self.activate_zoom()
+
         self._timer = app.Timer('auto', connect=self.on_timer, start=True)
-    
+
+        self.show()
+
     def on_timer(self, event):
         t = event.elapsed
-        self.program['u_time'] = t
         self.program['sphere_position_0'] = (+.75, .1, 2.0 + 1.0 * cos(4*t))
         self.program['sphere_position_1'] = (-.75, .1, 2.0 - 1.0 * cos(4*t))
         self.update()
 
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        width, height = self.size
+        gloo.set_viewport(0, 0, *self.physical_size)
         self.program['u_aspect_ratio'] = width/float(height)
 
     def on_draw(self, event):
@@ -251,5 +255,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     canvas = Canvas()
-    canvas.show()
     app.run()
diff --git a/examples/demo/gloo/realtime_signals.py b/examples/demo/gloo/realtime_signals.py
index 07c8839..8cb2076 100644
--- a/examples/demo/gloo/realtime_signals.py
+++ b/examples/demo/gloo/realtime_signals.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 # vispy: gallery 2
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -29,7 +29,7 @@ amplitudes = .1 + .2 * np.random.rand(m, 1).astype(np.float32)
 # Generate the signals as a (m, n) array.
 y = amplitudes * np.random.randn(m, n).astype(np.float32)
 
-# Color of each vertex (TODO: make it more efficient by using a GLSL-based 
+# Color of each vertex (TODO: make it more efficient by using a GLSL-based
 # color map and the index).
 color = np.repeat(np.random.uniform(size=(m, 3), low=.5, high=.9),
                   n, axis=0).astype(np.float32)
@@ -70,21 +70,21 @@ varying vec4 v_ab;
 void main() {
     float nrows = u_size.x;
     float ncols = u_size.y;
-   
+
     // Compute the x coordinate from the time index.
     float x = -1 + 2*a_index.z / (u_n-1);
-    vec2 position = vec2(x, a_position);
-    
+    vec2 position = vec2(x - (1 - 1 / u_scale.x), a_position);
+
     // Find the affine transformation for the subplots.
     vec2 a = vec2(1./ncols, 1./nrows)*.9;
-    vec2 b = vec2(-1 + 2*(a_index.x+.5) / ncols, 
+    vec2 b = vec2(-1 + 2*(a_index.x+.5) / ncols,
                   -1 + 2*(a_index.y+.5) / nrows);
     // Apply the static subplot transformation + scaling.
     gl_Position = vec4(a*u_scale*position+b, 0.0, 1.0);
-    
+
     v_color = vec4(a_color, 1.);
     v_index = a_index;
-    
+
     // For clipping test in the fragment shader.
     v_position = gl_Position.xy;
     v_ab = vec4(a, b);
@@ -102,11 +102,11 @@ varying vec4 v_ab;
 
 void main() {
     gl_FragColor = v_color;
-    
+
     // Discard the fragments between the signals (emulate glMultiDrawArrays).
     if ((fract(v_index.x) > 0.) || (fract(v_index.y) > 0.))
         discard;
-      
+
     // Clipping test.
     vec2 test = abs((v_position.xy-v_ab.zw)/v_ab.xy);
     if ((test.x > 1) || (test.y > 1))
@@ -117,30 +117,32 @@ void main() {
 
 class Canvas(app.Canvas):
     def __init__(self):
-        app.Canvas.__init__(self, title='Use your wheel to zoom!', 
+        app.Canvas.__init__(self, title='Use your wheel to zoom!',
                             keys='interactive')
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
-        self.program['a_position'] = y.ravel()
+        self.program['a_position'] = y.reshape(-1, 1)
         self.program['a_color'] = color
         self.program['a_index'] = index
         self.program['u_scale'] = (1., 1.)
         self.program['u_size'] = (nrows, ncols)
         self.program['u_n'] = n
-        
+
+        gloo.set_viewport(0, 0, *self.physical_size)
+
         self._timer = app.Timer('auto', connect=self.on_timer, start=True)
-    
-    def on_initialize(self, event):
-        gloo.set_state(clear_color='black', blend=True, 
+
+        gloo.set_state(clear_color='black', blend=True,
                        blend_func=('src_alpha', 'one_minus_src_alpha'))
 
+        self.show()
+
     def on_resize(self, event):
-        self.width, self.height = event.size
-        gloo.set_viewport(0, 0, self.width, self.height)
-        
+        gloo.set_viewport(0, 0, *event.physical_size)
+
     def on_mouse_wheel(self, event):
         dx = np.sign(event.delta[1]) * .05
-        scale_x, scale_y = self.program['u_scale']     
-        scale_x_new, scale_y_new = (scale_x * math.exp(2.5*dx), 
+        scale_x, scale_y = self.program['u_scale']
+        scale_x_new, scale_y_new = (scale_x * math.exp(2.5*dx),
                                     scale_y * math.exp(0.0*dx))
         self.program['u_scale'] = (max(1, scale_x_new), max(1, scale_y_new))
         self.update()
@@ -150,15 +152,14 @@ class Canvas(app.Canvas):
         k = 10
         y[:, :-k] = y[:, k:]
         y[:, -k:] = amplitudes * np.random.randn(m, k)
-        
+
         self.program['a_position'].set_data(y.ravel().astype(np.float32))
         self.update()
-        
+
     def on_draw(self, event):
         gloo.clear()
         self.program.draw('line_strip')
-        
+
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/shadertoy.py b/examples/demo/gloo/shadertoy.py
new file mode 100644
index 0000000..1efdc85
--- /dev/null
+++ b/examples/demo/gloo/shadertoy.py
@@ -0,0 +1,441 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: gallery 2, testskip
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+Shadertoy demo. You can copy-paste shader code from an example on
+www.shadertoy.com and get the demo.
+
+TODO: support cubes and videos as channel inputs (currently, only images
+are supported).
+
+"""
+
+# NOTE: This example throws warnings about variables not being used;
+# this is normal because only some shadertoy examples make use of all
+# variables, and the GPU may compile some of them away.
+
+import sys
+from datetime import datetime, time
+import numpy as np
+from vispy import gloo
+from vispy import app
+
+
+vertex = """
+#version 120
+
+attribute vec2 position;
+void main()
+{
+    gl_Position = vec4(position, 0.0, 1.0);
+}
+"""
+
+fragment = """
+#version 120
+
+uniform vec3      iResolution;           // viewport resolution (in pixels)
+uniform float     iGlobalTime;           // shader playback time (in seconds)
+uniform vec4      iMouse;                // mouse pixel coords
+uniform vec4      iDate;                 // (year, month, day, time in seconds)
+uniform float     iSampleRate;           // sound sample rate (i.e., 44100)
+uniform sampler2D iChannel0;             // input channel. XX = 2D/Cube
+uniform sampler2D iChannel1;             // input channel. XX = 2D/Cube
+uniform sampler2D iChannel2;             // input channel. XX = 2D/Cube
+uniform sampler2D iChannel3;             // input channel. XX = 2D/Cube
+uniform vec3      iChannelResolution[4]; // channel resolution (in pixels)
+uniform float     iChannelTime[4];       // channel playback time (in sec)
+
+%s
+"""
+
+
+def get_idate():
+    now = datetime.now()
+    utcnow = datetime.utcnow()
+    midnight_utc = datetime.combine(utcnow.date(), time(0))
+    delta = utcnow - midnight_utc
+    return (now.year, now.month, now.day, delta.seconds)
+
+
+def noise(resolution=64, nchannels=1):
+    # Random texture.
+    return np.random.randint(low=0, high=256,
+                             size=(resolution, resolution, nchannels)
+                             ).astype(np.uint8)
+
+
+class Canvas(app.Canvas):
+
+    def __init__(self, shadertoy=None):
+        app.Canvas.__init__(self, keys='interactive')
+        if shadertoy is None:
+            shadertoy = """
+            void main(void)
+            {
+                vec2 uv = gl_FragCoord.xy / iResolution.xy;
+                gl_FragColor = vec4(uv,0.5+0.5*sin(iGlobalTime),1.0);
+            }"""
+        self.program = gloo.Program(vertex, fragment % shadertoy)
+
+        self.program["position"] = [(-1, -1), (-1, 1), (1, 1),
+                                    (-1, -1), (1, 1), (1, -1)]
+        self.program['iMouse'] = 0, 0, 0, 0
+
+        self.program['iSampleRate'] = 44100.
+        for i in range(4):
+            self.program['iChannelTime[%d]' % i] = 0.
+
+        self.activate_zoom()
+
+        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
+
+        self.show()
+
+    def set_channel_input(self, img, i=0):
+        tex = gloo.Texture2D(img)
+        tex.interpolation = 'linear'
+        tex.wrapping = 'repeat'
+        self.program['iChannel%d' % i] = tex
+        self.program['iChannelResolution[%d]' % i] = img.shape
+
+    def on_draw(self, event):
+        self.program.draw()
+
+    def on_mouse_click(self, event):
+        # BUG: DOES NOT WORK YET, NO CLICK EVENT IN VISPY FOR NOW...
+        imouse = event.pos + event.pos
+        self.program['iMouse'] = imouse
+
+    def on_mouse_move(self, event):
+        if event.is_dragging:
+            x, y = event.pos
+            px, py = event.press_event.pos
+            imouse = (x, self.size[1] - y, px, self.size[1] - py)
+            self.program['iMouse'] = imouse
+
+    def on_timer(self, event):
+        self.program['iGlobalTime'] = event.elapsed
+        self.program['iDate'] = get_idate()  # used in some shadertoy exs
+        self.update()
+
+    def on_resize(self, event):
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        gloo.set_viewport(0, 0, *self.physical_size)
+        self.program['iResolution'] = (self.physical_size[0],
+                                       self.physical_size[1], 0.)
+
+# -------------------------------------------------------------------------
+# COPY-PASTE SHADERTOY CODE BELOW
+# -------------------------------------------------------------------------
+SHADERTOY = """
+// From: https://www.shadertoy.com/view/MdX3Rr
+
+// Created by inigo quilez - iq/2013
+// License Creative Commons Attribution-NonCommercial-ShareAlike 3.0
+// Unported License.
+
+//stereo thanks to Croqueteer
+//#define STEREO
+
+// value noise, and its analytical derivatives
+vec3 noised( in vec2 x )
+{
+    vec2 p = floor(x);
+    vec2 f = fract(x);
+
+    vec2 u = f*f*(3.0-2.0*f);
+
+    float a = texture2D(iChannel0,(p+vec2(0.5,0.5))/256.0,-100.0).x;
+    float b = texture2D(iChannel0,(p+vec2(1.5,0.5))/256.0,-100.0).x;
+    float c = texture2D(iChannel0,(p+vec2(0.5,1.5))/256.0,-100.0).x;
+    float d = texture2D(iChannel0,(p+vec2(1.5,1.5))/256.0,-100.0).x;
+
+    return vec3(a+(b-a)*u.x+(c-a)*u.y+(a-b-c+d)*u.x*u.y,
+                6.0*f*(1.0-f)*(vec2(b-a,c-a)+(a-b-c+d)*u.yx));
+}
+
+const mat2 m2 = mat2(0.8,-0.6,0.6,0.8);
+
+float terrain( in vec2 x )
+{
+    vec2  p = x*0.003;
+    float a = 0.0;
+    float b = 1.0;
+    vec2  d = vec2(0.0);
+    for( int i=0; i<6; i++ )
+    {
+        vec3 n = noised(p);
+        d += n.yz;
+        a += b*n.x/(1.0+dot(d,d));
+        b *= 0.5;
+        p = m2*p*2.0;
+    }
+
+    return 140.0*a;
+}
+
+float terrain2( in vec2 x )
+{
+    vec2  p = x*0.003;
+    float a = 0.0;
+    float b = 1.0;
+    vec2  d = vec2(0.0);
+    for( int i=0; i<14; i++ )
+    {
+        vec3 n = noised(p);
+        d += n.yz;
+        a += b*n.x/(1.0+dot(d,d));
+        b *= 0.5;
+        p=m2*p*2.0;
+    }
+
+    return 140.0*a;
+}
+
+float terrain3( in vec2 x )
+{
+    vec2  p = x*0.003;
+    float a = 0.0;
+    float b = 1.0;
+    vec2  d = vec2(0.0);
+    for( int i=0; i<4; i++ )
+    {
+        vec3 n = noised(p);
+        d += n.yz;
+        a += b*n.x/(1.0+dot(d,d));
+        b *= 0.5;
+        p = m2*p*2.0;
+    }
+
+    return 140.0*a;
+}
+
+float map( in vec3 p )
+{
+    float h = terrain(p.xz);
+    return p.y - h;
+}
+
+float map2( in vec3 p )
+{
+    float h = terrain2(p.xz);
+    return p.y - h;
+}
+
+float interesct( in vec3 ro, in vec3 rd )
+{
+    float h = 1.0;
+    float t = 1.0;
+    for( int i=0; i<120; i++ )
+    {
+        if( h<0.01 || t>2000.0 ) break;
+        t += 0.5*h;
+        h = map( ro + t*rd );
+    }
+
+    if( t>2000.0 ) t = -1.0;
+    return t;
+}
+
+float sinteresct(in vec3 ro, in vec3 rd )
+{
+#if 0
+    // no shadows
+    return 1.0;
+#endif
+
+#if 0
+    // fake shadows
+    vec3 nor;
+    vec3  eps = vec3(20.0,0.0,0.0);
+    nor.x = terrain3(ro.xz-eps.xy) - terrain3(ro.xz+eps.xy);
+    nor.y = 1.0*eps.x;
+    nor.z = terrain3(ro.xz-eps.yx) - terrain3(ro.xz+eps.yx);
+    nor = normalize(nor);
+    return clamp( 4.0*dot(nor,rd), 0.0, 1.0 );
+#endif
+
+#if 1
+    // real shadows
+    float res = 1.0;
+    float t = 0.0;
+    for( int j=0; j<48; j++ )
+    {
+        vec3 p = ro + t*rd;
+        float h = map( p );
+        res = min( res, 16.0*h/t );
+        t += h;
+        if( res<0.001 ||p.y>300.0 ) break;
+    }
+
+    return clamp( res, 0.0, 1.0 );
+#endif
+}
+
+vec3 calcNormal( in vec3 pos, float t )
+{
+    float e = 0.001;
+    e = 0.001*t;
+    vec3  eps = vec3(e,0.0,0.0);
+    vec3 nor;
+#if 0
+    nor.x = map2(pos+eps.xyy) - map2(pos-eps.xyy);
+    nor.y = map2(pos+eps.yxy) - map2(pos-eps.yxy);
+    nor.z = map2(pos+eps.yyx) - map2(pos-eps.yyx);
+#else
+    nor.x = terrain2(pos.xz-eps.xy) - terrain2(pos.xz+eps.xy);
+    nor.y = 2.0*e;
+    nor.z = terrain2(pos.xz-eps.yx) - terrain2(pos.xz+eps.yx);
+#endif
+    return normalize(nor);
+}
+
+vec3 camPath( float time )
+{
+    vec2 p = 1100.0*vec2( cos(0.0+0.23*time), cos(1.5+0.21*time) );
+
+    return vec3( p.x, 0.0, p.y );
+}
+
+
+float fbm( vec2 p )
+{
+    float f = 0.0;
+
+    f += 0.5000*texture2D( iChannel0, p/256.0 ).x; p = m2*p*2.02;
+    f += 0.2500*texture2D( iChannel0, p/256.0 ).x; p = m2*p*2.03;
+    f += 0.1250*texture2D( iChannel0, p/256.0 ).x; p = m2*p*2.01;
+    f += 0.0625*texture2D( iChannel0, p/256.0 ).x;
+
+    return f/0.9375;
+}
+
+
+void main(void)
+{
+    vec2 xy = -1.0 + 2.0*gl_FragCoord.xy / iResolution.xy;
+
+    vec2 s = xy*vec2(iResolution.x/iResolution.y,1.0);
+
+    #ifdef STEREO
+    float isCyan = mod(gl_FragCoord.x + mod(gl_FragCoord.y,2.0),2.0);
+    #endif
+
+    float time = iGlobalTime*0.15 + 0.3 + 4.0*iMouse.x/iResolution.x;
+
+    vec3 light1 = normalize( vec3(-0.8,0.4,-0.3) );
+
+
+
+    vec3 ro = camPath( time );
+    vec3 ta = camPath( time + 3.0 );
+    ro.y = terrain3( ro.xz ) + 11.0;
+    ta.y = ro.y - 20.0;
+
+    float cr = 0.2*cos(0.1*time);
+    vec3  cw = normalize(ta-ro);
+    vec3  cp = vec3(sin(cr), cos(cr),0.0);
+    vec3  cu = normalize( cross(cw,cp) );
+    vec3  cv = normalize( cross(cu,cw) );
+    vec3  rd = normalize( s.x*cu + s.y*cv + 2.0*cw );
+
+    #ifdef STEREO
+    ro += 2.0*cu*isCyan;
+    #endif
+
+    float sundot = clamp(dot(rd,light1),0.0,1.0);
+    vec3 col;
+    float t = interesct( ro, rd );
+    if( t<0.0 )
+    {
+        // sky
+        col = vec3(0.3,.55,0.8)*(1.0-0.8*rd.y);
+        col += 0.25*vec3(1.0,0.7,0.4)*pow( sundot,5.0 );
+        col += 0.25*vec3(1.0,0.8,0.6)*pow( sundot,64.0 );
+        col += 0.2*vec3(1.0,0.8,0.6)*pow( sundot,512.0 );
+        vec2 sc = ro.xz + rd.xz*(1000.0-ro.y)/rd.y;
+        col = mix( col, vec3(1.0,0.95,1.0),
+            0.5*smoothstep(0.5,0.8,fbm(0.0005*sc)) );
+    }
+    else
+    {
+        // mountains
+        vec3 pos = ro + t*rd;
+
+        vec3 nor = calcNormal( pos, t );
+
+        float r = texture2D( iChannel0, 7.0*pos.xz/256.0 ).x;
+
+        col = (r*0.25+0.75)*0.9*mix( vec3(0.08,0.05,0.03),
+            vec3(0.10,0.09,0.08), texture2D(iChannel0,0.00007*vec2(
+                pos.x,pos.y*48.0)).x );
+        col = mix( col, 0.20*vec3(0.45,.30,0.15)*(0.50+0.50*r),
+            smoothstep(0.70,0.9,nor.y) );
+        col = mix( col, 0.15*vec3(0.30,.30,0.10)*(0.25+0.75*r),
+            smoothstep(0.95,1.0,nor.y) );
+
+        // snow
+        float h = smoothstep(55.0,80.0,pos.y + 25.0*fbm(0.01*pos.xz) );
+        float e = smoothstep(1.0-0.5*h,1.0-0.1*h,nor.y);
+        float o = 0.3 + 0.7*smoothstep(0.0,0.1,nor.x+h*h);
+        float s = h*e*o;
+        col = mix( col, 0.29*vec3(0.62,0.65,0.7), smoothstep(
+            0.1, 0.9, s ) );
+
+         // lighting
+        float amb = clamp(0.5+0.5*nor.y,0.0,1.0);
+        float dif = clamp( dot( light1, nor ), 0.0, 1.0 );
+        float bac = clamp( 0.2 + 0.8*dot( normalize(
+            vec3(-light1.x, 0.0, light1.z ) ), nor ), 0.0, 1.0 );
+        float sh = 1.0; if( dif>=0.0001 ) sh = sinteresct(
+            pos+light1*20.0,light1);
+
+        vec3 lin  = vec3(0.0);
+        lin += dif*vec3(7.00,5.00,3.00)*vec3( sh, sh*sh*0.5+0.5*sh,
+            sh*sh*0.8+0.2*sh );
+        lin += amb*vec3(0.40,0.60,0.80)*1.5;
+        lin += bac*vec3(0.40,0.50,0.60);
+        col *= lin;
+
+
+        float fo = 1.0-exp(-0.0005*t);
+        vec3 fco = 0.55*vec3(0.55,0.65,0.75) + 0.1*vec3(1.0,0.8,0.5)*pow(
+            sundot, 4.0 );
+        col = mix( col, fco, fo );
+
+        col += 0.3*vec3(1.0,0.8,0.4)*pow( sundot,
+                    8.0 )*(1.0-exp(-0.002*t));
+    }
+
+    col = pow(col,vec3(0.4545));
+
+    // vignetting
+    col *= 0.5 + 0.5*pow( (xy.x+1.0)*(xy.y+1.0)*(xy.x-1.0)*(xy.y-1.0),
+                         0.1 );
+
+    #ifdef STEREO
+    col *= vec3( isCyan, 1.0-isCyan, 1.0-isCyan );
+    #endif
+
+//	col *= smoothstep( 0.0, 2.0, iGlobalTime );
+
+    gl_FragColor=vec4(col,1.0);
+}
+"""
+# -------------------------------------------------------------------------
+
+canvas = Canvas(SHADERTOY)
+# Input data.
+canvas.set_channel_input(noise(resolution=256, nchannels=1), i=0)
+
+if __name__ == '__main__':
+
+    canvas.show()
+    if sys.flags.interactive == 0:
+        canvas.app.run()
diff --git a/examples/demo/gloo/show_markers.py b/examples/demo/gloo/show_markers.py
deleted file mode 100644
index 3e1460d..0000000
--- a/examples/demo/gloo/show_markers.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# vispy: gallery 2
-
-""" Display markers at different sizes and line thicknessess.
-"""
-
-import os
-import sys
-
-import numpy as np
-
-from vispy import app
-from vispy.util.transforms import ortho
-from vispy.gloo import Program, VertexBuffer
-from vispy import gloo
-
-sys.path.insert(0, os.path.dirname(__file__))
-import markers
-
-
-n = 540
-data = np.zeros(n, dtype=[('a_position', np.float32, 3),
-                          ('a_fg_color', np.float32, 4),
-                          ('a_bg_color', np.float32, 4),
-                          ('a_size', np.float32, 1),
-                          ('a_linewidth', np.float32, 1)])
-data['a_fg_color'] = 0, 0, 0, 1
-data['a_bg_color'] = 1, 1, 1, 1
-data['a_linewidth'] = 1
-u_antialias = 1
-
-radius, theta, dtheta = 255.0, 0.0, 5.5 / 180.0 * np.pi
-for i in range(500):
-    theta += dtheta
-    x = 256 + radius * np.cos(theta)
-    y = 256 + 32 + radius * np.sin(theta)
-    r = 10.1 - i * 0.02
-    radius -= 0.45
-    data['a_position'][i] = x, y, 0
-    data['a_size'][i] = 2 * r
-
-for i in range(40):
-    r = 4
-    thickness = (i + 1) / 10.0
-    x = 20 + i * 12.5 - 2 * r
-    y = 16
-    data['a_position'][500 + i] = x, y, 0
-    data['a_size'][500 + i] = 2 * r
-    data['a_linewidth'][500 + i] = thickness
-
-
-class Canvas(app.Canvas):
-
-    def __init__(self):
-        app.Canvas.__init__(self, keys='interactive')
-
-        # This size is used for comparison with agg (via matplotlib)
-        self.size = 512, 512 + 2 * 32
-        self.title = "Markers demo [press space to change marker]"
-
-        self.vbo = VertexBuffer(data)
-        self.view = np.eye(4, dtype=np.float32)
-        self.model = np.eye(4, dtype=np.float32)
-        self.projection = ortho(0, self.size[0], 0, self.size[1], -1, 1)
-        self.programs = [
-            Program(markers.vert, markers.frag + markers.tailed_arrow),
-            Program(markers.vert, markers.frag + markers.disc),
-            Program(markers.vert, markers.frag + markers.diamond),
-            Program(markers.vert, markers.frag + markers.square),
-            Program(markers.vert, markers.frag + markers.cross),
-            Program(markers.vert, markers.frag + markers.arrow),
-            Program(markers.vert, markers.frag + markers.vbar),
-            Program(markers.vert, markers.frag + markers.hbar),
-            Program(markers.vert, markers.frag + markers.clobber),
-            Program(markers.vert, markers.frag + markers.ring)]
-
-        for program in self.programs:
-            program.bind(self.vbo)
-            program["u_antialias"] = u_antialias,
-            program["u_size"] = 1
-            program["u_model"] = self.model
-            program["u_view"] = self.view
-            program["u_projection"] = self.projection
-        self.index = 0
-        self.program = self.programs[self.index]
-
-    def on_initialize(self, event):
-        gloo.set_state(depth_test=False, blend=True, clear_color='white',
-                       blend_func=('src_alpha', 'one_minus_src_alpha'))
-
-    def on_key_press(self, event):
-        if event.text == ' ':
-            self.index = (self.index + 1) % (len(self.programs))
-            self.program = self.programs[self.index]
-            self.program['u_projection'] = self.projection
-            self.program['u_size'] = self.u_size
-            self.update()
-
-    def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = ortho(0, width, 0, height, -100, 100)
-        self.u_size = width / 512.0
-        self.program['u_projection'] = self.projection
-        self.program['u_size'] = self.u_size
-
-    def on_draw(self, event):
-        gloo.clear()
-        self.program.draw('points')
-
-if __name__ == '__main__':
-    canvas = Canvas()
-    canvas.show()
-    app.run()
diff --git a/examples/demo/gloo/signals.py b/examples/demo/gloo/signals.py
index 548ea45..efee0b4 100644
--- a/examples/demo/gloo/signals.py
+++ b/examples/demo/gloo/signals.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 # vispy: gallery 2
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -47,7 +47,7 @@ uniform vec2 u_pan;
 uniform vec2 u_scale;
 
 void main() {
-    
+
     vec2 position_tr = u_scale * (a_position + u_pan);
     gl_Position = vec4(position_tr, 0.0, 1.0);
     v_color = a_color;
@@ -72,17 +72,19 @@ class Canvas(app.Canvas):
         app.Canvas.__init__(self, keys='interactive')
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
         self.program.bind(gloo.VertexBuffer(data))
-        
+
         self.program['u_pan'] = (0., 0.)
         self.program['u_scale'] = (1., 1.)
 
-    def on_initialize(self, event):
-        gloo.set_state(clear_color=(1, 1, 1, 1), blend=True, 
+        gloo.set_viewport(0, 0, *self.physical_size)
+
+        gloo.set_state(clear_color=(1, 1, 1, 1), blend=True,
                        blend_func=('src_alpha', 'one_minus_src_alpha'))
 
+        self.show()
+
     def on_resize(self, event):
-        self.width, self.height = event.size
-        gloo.set_viewport(0, 0, self.width, self.height)
+        gloo.set_viewport(0, 0, *event.physical_size)
 
     def on_draw(self, event):
         gloo.clear(color=(0.0, 0.0, 0.0, 1.0))
@@ -90,9 +92,9 @@ class Canvas(app.Canvas):
 
     def _normalize(self, x_y):
         x, y = x_y
-        w, h = float(self.width), float(self.height)
+        w, h = float(self.size[0]), float(self.size[1])
         return x/(w/2.)-1., y/(h/2.)-1.
-            
+
     def on_mouse_move(self, event):
         if event.is_dragging:
             x0, y0 = self._normalize(event.press_event.pos)
@@ -100,31 +102,30 @@ class Canvas(app.Canvas):
             x, y = self._normalize(event.pos)
             dx, dy = x - x1, -(y - y1)
             button = event.press_event.button
-            
+
             pan_x, pan_y = self.program['u_pan']
             scale_x, scale_y = self.program['u_scale']
-            
+
             if button == 1:
                 self.program['u_pan'] = (pan_x+dx/scale_x, pan_y+dy/scale_y)
             elif button == 2:
                 scale_x_new, scale_y_new = (scale_x * math.exp(2.5*dx),
                                             scale_y * math.exp(2.5*dy))
                 self.program['u_scale'] = (scale_x_new, scale_y_new)
-                self.program['u_pan'] = (pan_x - 
-                                         x0 * (1./scale_x - 1./scale_x_new), 
-                                         pan_y + 
+                self.program['u_pan'] = (pan_x -
+                                         x0 * (1./scale_x - 1./scale_x_new),
+                                         pan_y +
                                          y0 * (1./scale_y - 1./scale_y_new))
             self.update()
 
     def on_mouse_wheel(self, event):
         dx = np.sign(event.delta[1])*.05
-        scale_x, scale_y = self.program['u_scale']     
-        scale_x_new, scale_y_new = (scale_x * math.exp(2.5*dx), 
+        scale_x, scale_y = self.program['u_scale']
+        scale_x_new, scale_y_new = (scale_x * math.exp(2.5*dx),
                                     scale_y * math.exp(2.5*dx))
         self.program['u_scale'] = (scale_x_new, scale_y_new)
         self.update()
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/spacy.py b/examples/demo/gloo/spacy.py
index 7e95a1f..3f6025a 100644
--- a/examples/demo/gloo/spacy.py
+++ b/examples/demo/gloo/spacy.py
@@ -24,6 +24,7 @@ uniform mat4 u_model;
 uniform mat4 u_view;
 uniform mat4 u_projection;
 uniform float u_time_offset;
+uniform float u_pixel_scale;
 
 attribute vec3  a_position;
 attribute float a_offset;
@@ -31,7 +32,7 @@ attribute float a_offset;
 varying float v_pointsize;
 
 void main (void) {
-   
+
     vec3 pos = a_position;
     pos.z = pos.z - a_offset - u_time_offset;
     vec4 v_eye_position = u_view * u_model * vec4(pos, 1.0);
@@ -42,7 +43,7 @@ void main (void) {
     float radius = 1;
     vec4 corner = vec4(radius, radius, v_eye_position.z, v_eye_position.w);
     vec4  proj_corner = u_projection * corner;
-    gl_PointSize = 100.0 * proj_corner.x / proj_corner.w;
+    gl_PointSize = 100.0 * u_pixel_scale * proj_corner.x / proj_corner.w;
     v_pointsize = gl_PointSize;
 }
 """
@@ -60,7 +61,7 @@ void main()
 }
 """
 
-N = 100000  # Number of stars 
+N = 100000  # Number of stars
 SIZE = 100
 SPEED = 4.0  # time in seconds to go through one block
 NBLOCKS = 10
@@ -69,36 +70,39 @@ NBLOCKS = 10
 class Canvas(app.Canvas):
 
     def __init__(self):
-        app.Canvas.__init__(self, title='Spacy', keys='interactive')
-        self.size = 800, 600
-        
+        app.Canvas.__init__(self, title='Spacy', keys='interactive',
+                            size=(800, 600))
+
         self.program = gloo.Program(vertex, fragment)
         self.view = np.eye(4, dtype=np.float32)
         self.model = np.eye(4, dtype=np.float32)
-        self.projection = np.eye(4, dtype=np.float32)
-        
+
+        self.activate_zoom()
+
         self.timer = app.Timer('auto', connect=self.update, start=True)
-        
+
         # Set uniforms (some are set later)
         self.program['u_model'] = self.model
         self.program['u_view'] = self.view
-        
+        self.program['u_pixel_scale'] = self.pixel_scale
+
         # Set attributes
         self.program['a_position'] = np.zeros((N, 3), np.float32)
-        self.program['a_offset'] = np.zeros((N,), np.float32)
-        
+        self.program['a_offset'] = np.zeros((N, 1), np.float32)
+
         # Init
         self._timeout = 0
         self._active_block = 0
         for i in range(NBLOCKS):
             self._generate_stars()
         self._timeout = time.time() + SPEED
-    
-    def on_initialize(self, event):
+
         gloo.set_state(clear_color='black', depth_test=False,
                        blend=True, blend_equation='func_add',
                        blend_func=('src_alpha', 'one_minus_src_alpha'))
 
+        self.show()
+
     def on_key_press(self, event):
         if event.text == ' ':
             if self.timer.running:
@@ -107,8 +111,11 @@ class Canvas(app.Canvas):
                 self.timer.start()
 
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        width, height = self.size
+        gloo.set_viewport(0, 0, *self.physical_size)
         far = SIZE*(NBLOCKS-2)
         self.projection = perspective(25.0, width / float(height), 1.0, far)
         self.program['u_projection'] = self.projection
@@ -118,37 +125,37 @@ class Canvas(app.Canvas):
         # the time offset goes from 0 to size
         factor = (self._timeout - time.time()) / SPEED
         self.program['u_time_offset'] = -(1-factor) * SIZE
-        
+
         # Draw
         gloo.clear()
         self.program.draw('points')
-        
+
         # Build new starts if the first block is fully behind us
         if factor < 0:
             self._generate_stars()
-    
+
     def on_close(self, event):
         self.timer.stop()
-    
+
     def _generate_stars(self):
-        
+
         # Get number of stars in each block
         blocksize = N // NBLOCKS
-        
+
         # Update active block
         self._active_block += 1
         if self._active_block >= NBLOCKS:
             self._active_block = 0
-        
+
         # Create new position data for the active block
-        pos = np.zeros((blocksize, 3), 'float32') 
+        pos = np.zeros((blocksize, 3), 'float32')
         pos[:, :2] = np.random.normal(0.0, SIZE/2, (blocksize, 2))  # x-y
         pos[:, 2] = np.random.uniform(0, SIZE, (blocksize,))  # z
         start_index = self._active_block * blocksize
-        self.program['a_position'].set_subdata(pos, offset=start_index) 
-        
+        self.program['a_position'].set_subdata(pos, offset=start_index)
+
         #print(start_index)
-        
+
         # Set offsets - active block gets offset 0
         for i in range(NBLOCKS):
             val = i - self._active_block
@@ -156,13 +163,12 @@ class Canvas(app.Canvas):
                 val += NBLOCKS
             values = np.ones((blocksize, 1), 'float32') * val * SIZE
             start_index = i*blocksize
-            self.program['a_offset'].set_subdata(values, offset=start_index) 
-        
+            self.program['a_offset'].set_subdata(values, offset=start_index)
+
         # Reset timer
         self._timeout += SPEED
 
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/terrain.py b/examples/demo/gloo/terrain.py
index e0fd370..df5fcfd 100644
--- a/examples/demo/gloo/terrain.py
+++ b/examples/demo/gloo/terrain.py
@@ -1,5 +1,9 @@
 # -*- coding: utf-8 -*-
-# vispy: gallery 30
+# vispy: gallery 30, testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
 
 """ Terrain generation using diamond-square alogrithm
 and Scipy for Delaunay triangulation
@@ -7,8 +11,7 @@ and Scipy for Delaunay triangulation
 
 from vispy import gloo
 from vispy import app
-from vispy.util.transforms import perspective, translate, xrotate, yrotate
-from vispy.util.transforms import zrotate
+from vispy.util.transforms import perspective, translate, rotate
 import numpy as np
 from scipy.spatial import Delaunay
 
@@ -106,9 +109,8 @@ class Canvas(app.Canvas):
 
     def __init__(self):
         app.Canvas.__init__(self, keys='interactive')
-
         self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
-        #Sets the view to an appropriate position over the terrain
+        # Sets the view to an appropriate position over the terrain
         self.default_view = np.array([[0.8, 0.2, -0.48, 0],
                                      [-0.5, 0.3, -0.78, 0],
                                      [-0.01, 0.9, -0.3, 0],
@@ -127,9 +129,12 @@ class Canvas(app.Canvas):
 
         self.program['a_position'] = gloo.VertexBuffer(triangles)
 
-    def on_initialize(self, event):
+        self.activate_zoom()
+
         gloo.set_state(clear_color='black', depth_test=True)
 
+        self.show()
+
     def on_key_press(self, event):
         """Controls -
         a(A) - move left
@@ -176,19 +181,21 @@ class Canvas(app.Canvas):
         elif(event.text == ' '):
             self.view = self.default_view
 
-        translate(self.view, -self.translate[0], -self.translate[1],
-                  -self.translate[2])
-        xrotate(self.view, self.rotate[0])
-        yrotate(self.view, self.rotate[1])
-        zrotate(self.view, self.rotate[2])
-
+        self.view = self.view.dot(
+            translate(-np.array(self.translate)).dot(
+                rotate(self.rotate[0], (1, 0, 0)).dot(
+                    rotate(self.rotate[1], (0, 1, 0)).dot(
+                        rotate(self.rotate[2], (0, 0, 1))))))
         self.program['u_view'] = self.view
         self.update()
 
     def on_resize(self, event):
-        width, height = event.size
-        gloo.set_viewport(0, 0, width, height)
-        self.projection = perspective(60.0, width / float(height), 1.0, 100.0)
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        gloo.set_viewport(0, 0, *self.physical_size)
+        self.projection = perspective(60.0, self.size[0] /
+                                      float(self.size[1]), 1.0, 100.0)
         self.program['u_projection'] = self.projection
 
     def on_draw(self, event):
@@ -202,5 +209,4 @@ generate_points(8)
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/gloo/unstructured_2d.py b/examples/demo/gloo/unstructured_2d.py
index ede88ee..667da02 100644
--- a/examples/demo/gloo/unstructured_2d.py
+++ b/examples/demo/gloo/unstructured_2d.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
+# vispy: testskip
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author:   Per Rosengren
@@ -15,16 +16,15 @@ Takes unstructured 2D locations, with corresponding 1 or 2 dimensional
 scalar "values". Plots the values looked up from colormaps and
 interpolated between the locations.
 """
-
+import sys
 import numpy as np
-from vispy import gloo
-from vispy import app
-from vispy.util.transforms import ortho
-
 import scipy.spatial
 
+from vispy import gloo, app
+from vispy.util.transforms import ortho
+
 
-class Unstructured2d(app.Canvas):
+class Canvas(app.Canvas):
 
     def __init__(self,
                  x=None, y=None, u=None, v=None,
@@ -42,6 +42,10 @@ class Unstructured2d(app.Canvas):
         self._dir_x_right = dir_x_right
         self._dir_y_top = dir_y_top
 
+        self.activate_zoom()
+
+        self.show()
+
     def create_shader(self, colormap):
         if len(colormap.shape) == 2:
             args = dict(
@@ -54,8 +58,6 @@ class Unstructured2d(app.Canvas):
                 tex_t="vec2",
                 texture2D_arg="v_texcoord")
         vertex = """
-            uniform mat4 model;
-            uniform mat4 view;
             uniform mat4 projection;
             uniform sampler2D texture;
 
@@ -86,11 +88,7 @@ class Unstructured2d(app.Canvas):
         else:
             self.program['texture'] = colormap
         self.program['texture'].interpolation = 'linear'
-        self.view = np.eye(4, dtype=np.float32)
-        self.model = np.eye(4, dtype=np.float32)
         self.projection = np.eye(4, dtype=np.float32)
-        self.program['model'] = self.model
-        self.program['view'] = self.view
 
     def create_mesh(self, x, y, u, v):
         tri = scipy.spatial.Delaunay(np.column_stack([x, y]))
@@ -112,7 +110,6 @@ class Unstructured2d(app.Canvas):
         self.vbo = gloo.VertexBuffer(data)
         self.index = gloo.IndexBuffer(edges)
 
-    def on_initialize(self, event):
         gloo.set_state(blend=True, clear_color='white',
                        blend_func=('src_alpha', 'one_minus_src_alpha'))
 
@@ -121,10 +118,11 @@ class Unstructured2d(app.Canvas):
         self.program.draw('triangles', self.index)
 
     def on_resize(self, event):
-        self.resize(*event.size)
+        self.activate_zoom()
 
-    def resize(self, width, height):
-        gloo.set_viewport(0, 0, width, height)
+    def activate_zoom(self):
+        width, heigh = self.size
+        gloo.set_viewport(0, 0, *self.physical_size)
         data_width = self._data_lim[0][1] - self._data_lim[0][0]
         data_height = self._data_lim[1][1] - self._data_lim[1][0]
         data_aspect = data_width / float(data_height)
@@ -195,6 +193,7 @@ def create_colormap1d_hot(size=512):
     rgb[hs:, 2] = 1 - u
     return rgb
 
+
 if __name__ == '__main__':
     loc = np.random.random_sample(size=(100, 2))
     np.random.shuffle(loc)
@@ -203,16 +202,15 @@ if __name__ == '__main__':
     vec[:, 1] = np.cos(loc[:, 1] * 13)
     width = 500
     height = 500
-    c1 = Unstructured2d(title="Unstructured 2D - 2D colormap",
-                        size=(width, height), position=(0, 40),
-                        x=loc[:, 0], y=loc[:, 1], u=vec[:, 0], v=vec[:, 1],
-                        colormap=create_colormap2d_4dirs(size=128),
-                        keys='interactive')
-    c2 = Unstructured2d(title="Unstructured 2D - 1D colormap",
-                        size=(width, height), position=(width + 20, 40),
-                        x=loc[:, 0], y=loc[:, 1], u=vec[:, 0],
-                        colormap=create_colormap1d_hot(size=128),
-                        keys='interactive')
-    c1.show()
-    c2.show()
-    app.run()
+    c1 = Canvas(title="Unstructured 2D - 2D colormap",
+                size=(width, height), position=(0, 40),
+                x=loc[:, 0], y=loc[:, 1], u=vec[:, 0], v=vec[:, 1],
+                colormap=create_colormap2d_4dirs(size=128),
+                keys='interactive')
+    c2 = Canvas(title="Unstructured 2D - 1D colormap",
+                size=(width, height), position=(width + 20, 40),
+                x=loc[:, 0], y=loc[:, 1], u=vec[:, 0],
+                colormap=create_colormap1d_hot(size=128),
+                keys='interactive')
+    if sys.flags.interactive == 0:
+        app.run()
diff --git a/examples/demo/gloo/voronoi.py b/examples/demo/gloo/voronoi.py
index 1ba0415..92e5359 100644
--- a/examples/demo/gloo/voronoi.py
+++ b/examples/demo/gloo/voronoi.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 # vispy: gallery 30
+# vispy: testskip - because this example sometimes sets inactive attributes
 """Computing a Voronoi diagram on the GPU. Shows how to use uniform arrays.
 
 Original version by Xavier Olive (xoolive).
@@ -44,10 +45,11 @@ void main() {
 # Seed point shaders.
 VS_seeds = """
 attribute vec2 a_position;
+uniform float u_ps;
 
 void main() {
     gl_Position = vec4(2. * a_position - 1., 0., 1.);
-    gl_PointSize = 10.;
+    gl_PointSize = 10. * u_ps;
 }
 """
 
@@ -63,24 +65,31 @@ class Canvas(app.Canvas):
     def __init__(self):
         app.Canvas.__init__(self, size=(600, 600), title='Voronoi diagram',
                             keys='interactive')
-        
-        self.seeds = np.random.uniform(0, 1,
+
+        self.ps = self.pixel_scale
+
+        self.seeds = np.random.uniform(0, 1.0*self.ps,
                                        size=(32, 2)).astype(np.float32)
-        self.colors = np.random.uniform(0.3, 0.8, 
+        self.colors = np.random.uniform(0.3, 0.8,
                                         size=(32, 3)).astype(np.float32)
-        
+
         # Set Voronoi program.
         self.program_v = gloo.Program(VS_voronoi, FS_voronoi)
         self.program_v['a_position'] = [(-1, -1), (-1, +1), (+1, -1), (+1, +1)]
-        # HACK: work-around a bug related to uniform arrays until 
+        # HACK: work-around a bug related to uniform arrays until
         # issue #345 is solved.
         for i in range(32):
             self.program_v['u_seeds[%d]' % i] = self.seeds[i, :]
             self.program_v['u_colors[%d]' % i] = self.colors[i, :]
-            
+
         # Set seed points program.
         self.program_s = gloo.Program(VS_seeds, FS_seeds)
         self.program_s['a_position'] = self.seeds
+        self.program_s['u_ps'] = self.ps
+
+        self.activate_zoom()
+
+        self.show()
 
     def on_draw(self, event):
         gloo.clear()
@@ -88,14 +97,17 @@ class Canvas(app.Canvas):
         self.program_s.draw('points')
 
     def on_resize(self, event):
-        self.width, self.height = event.size
-        gloo.set_viewport(0, 0, self.width, self.height)
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        self.width, self.height = self.size
+        gloo.set_viewport(0, 0, *self.physical_size)
         self.program_v['u_screen'] = (self.width, self.height)
-        
+
     def on_mouse_move(self, event):
         x, y = event.pos
         x, y = x/float(self.width), 1-y/float(self.height)
-        self.program_v['u_seeds[0]'] = x, y
+        self.program_v['u_seeds[0]'] = x*self.ps, y*self.ps
         # TODO: just update the first line in the VBO instead of uploading the
         # whole array of seed points.
         self.seeds[0, :] = x, y
@@ -104,5 +116,4 @@ class Canvas(app.Canvas):
 
 if __name__ == "__main__":
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/demo/scene/isocurve_for_trisurface_qt.py b/examples/demo/scene/isocurve_for_trisurface_qt.py
new file mode 100644
index 0000000..2fc2a92
--- /dev/null
+++ b/examples/demo/scene/isocurve_for_trisurface_qt.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+"""
+This example demonstrates isocurve for triangular mesh with vertex data and a
+ qt interface.
+"""
+
+import sys
+import numpy as np
+
+from vispy import scene
+
+from vispy.geometry.generation import create_sphere
+from vispy.color.colormap import get_colormaps
+
+try:
+    from sip import setapi
+    setapi("QVariant", 2)
+    setapi("QString", 2)
+except ImportError:
+    pass
+
+from PyQt4 import QtGui, QtCore
+
+
+class ObjectWidget(QtGui.QWidget):
+    """
+    Widget for editing OBJECT parameters
+    """
+    signal_objet_changed = QtCore.pyqtSignal(name='objectChanged')
+
+    def __init__(self, parent=None):
+        super(ObjectWidget, self).__init__(parent)
+
+        l_nbr_steps = QtGui.QLabel("Nbr Steps ")
+        self.nbr_steps = QtGui.QSpinBox()
+        self.nbr_steps.setMinimum(3)
+        self.nbr_steps.setMaximum(100)
+        self.nbr_steps.setValue(6)
+        self.nbr_steps.valueChanged.connect(self.update_param)
+
+        l_cmap = QtGui.QLabel("Cmap ")
+        self.cmap = list(get_colormaps().keys())
+        self.combo = QtGui.QComboBox(self)
+        self.combo.addItems(self.cmap)
+        self.combo.currentIndexChanged.connect(self.update_param)
+
+        gbox = QtGui.QGridLayout()
+        gbox.addWidget(l_cmap, 0, 0)
+        gbox.addWidget(self.combo, 0, 1)
+        gbox.addWidget(l_nbr_steps, 1, 0)
+        gbox.addWidget(self.nbr_steps, 1, 1)
+
+        vbox = QtGui.QVBoxLayout()
+        vbox.addLayout(gbox)
+        vbox.addStretch(1.0)
+
+        self.setLayout(vbox)
+
+    def update_param(self, option):
+        self.signal_objet_changed.emit()
+
+
+class MainWindow(QtGui.QMainWindow):
+
+    def __init__(self):
+        QtGui.QMainWindow.__init__(self)
+
+        self.resize(700, 500)
+        self.setWindowTitle('vispy example ...')
+
+        splitter = QtGui.QSplitter(QtCore.Qt.Horizontal)
+
+        self.canvas = Canvas()
+        self.canvas.create_native()
+        self.canvas.native.setParent(self)
+
+        self.props = ObjectWidget()
+        splitter.addWidget(self.props)
+        splitter.addWidget(self.canvas.native)
+
+        self.setCentralWidget(splitter)
+        self.props.signal_objet_changed.connect(self.update_view)
+        self.update_view()
+
+    def update_view(self):
+        # banded, nbr_steps, cmap
+        self.canvas.set_data(self.props.nbr_steps.value(),
+                             self.props.combo.currentText())
+
+
+class Canvas(scene.SceneCanvas):
+
+    def __init__(self):
+        scene.SceneCanvas.__init__(self, keys='interactive')
+        self.size = 800, 600
+        self.view = self.central_widget.add_view()
+        self.view.camera = scene.TurntableCamera()
+        self.radius = 2.0
+        mesh = create_sphere(20, 20, radius=self.radius)
+        vertices = mesh.get_vertices()
+        tris = mesh.get_faces()
+
+        cl = np.linspace(-self.radius, self.radius, 6 + 2)[1:-1]
+
+        self.iso = scene.visuals.Isoline(vertices=vertices, tris=tris,
+                                         data=vertices[:, 2],
+                                         levels=cl, color_lev='autumn',
+                                         parent=self.view.scene)
+
+        # Add a 3D axis to keep us oriented
+        scene.visuals.XYZAxis(parent=self.view.scene)
+
+    def set_data(self, n_levels, cmap):
+        self.iso.set_color(cmap)
+        cl = np.linspace(-self.radius, self.radius, n_levels + 2)[1:-1]
+        self.iso.set_levels(cl)
+
+
+# -----------------------------------------------------------------------------
+if __name__ == '__main__':
+    appQt = QtGui.QApplication(sys.argv)
+    win = MainWindow()
+    win.show()
+    appQt.exec_()
diff --git a/examples/demo/scene/magnify.py b/examples/demo/scene/magnify.py
new file mode 100644
index 0000000..f844a5c
--- /dev/null
+++ b/examples/demo/scene/magnify.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+# vispy: gallery 10
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+""" Demonstrates use of special Camera subclasses to implement a (flashy) 
+data-exploration tool.
+
+Here we use MagnifyCamera to allow the user to zoom in on a particular 
+region of data, while also keeping the entire data set visible for reference.
+
+The MagnifyCamera classes are responsible for inserting MagnifyTransform
+at the transform of each viewbox scene, while also updating those transforms
+to respond to user input.
+"""
+
+import numpy as np
+import vispy.scene
+from vispy.scene import visuals
+from vispy.scene.cameras import MagnifyCamera, Magnify1DCamera
+from vispy.visuals.transforms import STTransform
+from vispy.util import filter 
+
+# 
+# Make a canvas and partition it into 3 viewboxes.
+#
+canvas = vispy.scene.SceneCanvas(keys='interactive', show=True)
+grid = canvas.central_widget.add_grid()
+
+vb1 = grid.add_view(row=0, col=0, col_span=2)
+vb2 = grid.add_view(row=1, col=0)
+vb3 = grid.add_view(row=1, col=1)
+
+#
+# Top viewbox: Show a plot line containing fine structure with a 1D 
+# magnification transform.
+#
+
+
+pos = np.empty((100000, 2))
+pos[:, 0] = np.arange(100000)
+pos[:, 1] = np.random.normal(size=100000, loc=50, scale=10)
+pos[:, 1] = filter.gaussian_filter(pos[:, 1], 20)
+pos[:, 1] += np.random.normal(size=100000, loc=0, scale=2)
+pos[:, 1][pos[:, 1] > 55] += 100
+pos[:, 1] = filter.gaussian_filter(pos[:, 1], 2)
+line = visuals.Line(pos, color='white', parent=vb1.scene)
+line.transform = STTransform(translate=(0, 0, -0.1))
+
+grid1 = visuals.GridLines(parent=vb1.scene)
+
+vb1.camera = Magnify1DCamera(mag=4, size_factor=0.6, radius_ratio=0.6)
+vb1.camera.rect = 0, 30, 100000, 100
+
+#
+# Bottom-left viewbox: Image with circular magnification lens.
+#
+size = (100, 100)
+
+img_data = np.random.normal(size=size+(3,), loc=58, scale=20).astype(np.ubyte)
+image = visuals.Image(img_data, parent=vb2.scene, method='impostor')
+vb2.camera = MagnifyCamera(mag=3, size_factor=0.3, radius_ratio=0.6)
+vb2.camera.rect = (-10, -10, size[0]+20, size[1]+20) 
+
+#
+# Bottom-right viewbox: Scatter plot with many clusters of varying scale.
+#
+
+
+centers = np.random.normal(size=(50, 2))
+pos = np.random.normal(size=(100000, 2), scale=0.2)
+indexes = np.random.normal(size=100000, loc=centers.shape[0]/2., 
+                           scale=centers.shape[0]/3.)
+indexes = np.clip(indexes, 0, centers.shape[0]-1).astype(int)
+scales = 10**(np.linspace(-2, 0.5, centers.shape[0]))[indexes][:, np.newaxis]
+pos *= scales
+pos += centers[indexes]
+
+scatter = visuals.Markers()
+scatter.set_data(pos, edge_color=None, face_color=(1, 1, 1, 0.3), size=5)
+vb3.add(scatter)
+
+grid2 = visuals.GridLines(parent=vb3.scene)
+vb3.camera = MagnifyCamera(mag=3, size_factor=0.3, radius_ratio=0.9)
+vb3.camera.rect = (-5, -5, 10, 10)
+
+# Add helpful text
+text1 = visuals.Text("mouse wheel = magnify", pos=(100, 15), font_size=14, 
+                     color='white', parent=canvas.scene)
+text2 = visuals.Text("right button = zoom", pos=(100, 30), font_size=14, 
+                     color='white', parent=canvas.scene)
+
+
+if __name__ == '__main__':
+    import sys
+    if sys.flags.interactive != 1:
+        vispy.app.run()
diff --git a/examples/ipynb/colormaps.ipynb b/examples/ipynb/colormaps.ipynb
new file mode 100644
index 0000000..e042c1a
--- /dev/null
+++ b/examples/ipynb/colormaps.ipynb
@@ -0,0 +1,163 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# VisPy colormaps"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This notebook illustrates the colormap API provided by VisPy."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## List all colormaps"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "from vispy.color import (get_colormap, get_colormaps, Colormap)\n",
+    "from IPython.display import display_html"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "for cmap in get_colormaps():\n",
+    "    display_html('<h3>%s</h3>' % cmap, raw=True)\n",
+    "    display_html(get_colormap(cmap))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Discrete colormaps"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Discrete colormaps can be created by giving a list of colors, and an optional list of control points (in $[0,1]$, the first and last points need to be $0$ and $1$ respectively). The colors can be specified in many ways (1-character shortcuts, hexadecimal values, arrays or RGB values, `ColorArray` instances, and so on)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "Colormap(['r', 'g', 'b'], interpolation='zero')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "Colormap(['r', 'g', 'y'], interpolation='zero')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "Colormap(np.array([[0, .75, 0],\n",
+    "                   [.75, .25, .5]]), \n",
+    "         [0., .25, 1.], \n",
+    "         interpolation='zero')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "Colormap(['r', 'g', '#123456'],\n",
+    "         interpolation='zero')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Linear gradients"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "Colormap(['r', 'g', '#123456'])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "Colormap([[1,0,0], [1,1,1], [1,0,1]], \n",
+    "               [0., .75, 1.])"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.4.2"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/examples/ipynb/display_points.ipynb b/examples/ipynb/display_points.ipynb
deleted file mode 100644
index 7b78309..0000000
--- a/examples/ipynb/display_points.ipynb
+++ /dev/null
@@ -1,152 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:059d3c040693141e0b6069792e415d648bb28645da2b4afbcd3b02dfc9a12473"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "from vispy import gloo\n",
-      "from vispy import app, use\n",
-      "import numpy as np"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "use('ipynb_static')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "# Create vetices\n",
-      "n = 10000\n",
-      "v_position = 0.25 * np.random.randn(n, 2).astype(np.float32)\n",
-      "v_color = np.random.uniform(0, 1, (n, 3)).astype(np.float32)\n",
-      "v_size = np.random.uniform(2, 12, (n, 1)).astype(np.float32)\n",
-      "\n",
-      "VERT_SHADER = \"\"\"\n",
-      "attribute vec3  a_position;\n",
-      "attribute vec3  a_color;\n",
-      "attribute float a_size;\n",
-      "\n",
-      "varying vec4 v_fg_color;\n",
-      "varying vec4 v_bg_color;\n",
-      "varying float v_radius;\n",
-      "varying float v_linewidth;\n",
-      "varying float v_antialias;\n",
-      "\n",
-      "void main (void) {\n",
-      "    v_radius = a_size;\n",
-      "    v_linewidth = 1.0;\n",
-      "    v_antialias = 1.0;\n",
-      "    v_fg_color  = vec4(0.0,0.0,0.0,0.5);\n",
-      "    v_bg_color  = vec4(a_color,    1.0);\n",
-      "\n",
-      "    gl_Position = vec4(a_position, 1.0);\n",
-      "    gl_PointSize = 2.0*(v_radius + v_linewidth + 1.5*v_antialias);\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "FRAG_SHADER = \"\"\"\n",
-      "#version 120\n",
-      "\n",
-      "varying vec4 v_fg_color;\n",
-      "varying vec4 v_bg_color;\n",
-      "varying float v_radius;\n",
-      "varying float v_linewidth;\n",
-      "varying float v_antialias;\n",
-      "void main()\n",
-      "{\n",
-      "    float size = 2.0*(v_radius + v_linewidth + 1.5*v_antialias);\n",
-      "    float t = v_linewidth/2.0-v_antialias;\n",
-      "    float r = length((gl_PointCoord.xy - vec2(0.5,0.5))*size);\n",
-      "    float d = abs(r - v_radius) - t;\n",
-      "    if( d < 0.0 )\n",
-      "        gl_FragColor = v_fg_color;\n",
-      "    else\n",
-      "    {\n",
-      "        float alpha = d/v_antialias;\n",
-      "        alpha = exp(-alpha*alpha);\n",
-      "        if (r > v_radius)\n",
-      "            gl_FragColor = vec4(v_fg_color.rgb, alpha*v_fg_color.a);\n",
-      "        else\n",
-      "            gl_FragColor = mix(v_bg_color, v_fg_color, alpha);\n",
-      "    }\n",
-      "}\n",
-      "\"\"\""
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Points(app.Canvas):\n",
-      "\n",
-      "    def __init__(self, *args, **kwargs):\n",
-      "        app.Canvas.__init__(self, *args, **kwargs)\n",
-      "    \n",
-      "    def on_initialize(self, event):\n",
-      "        self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)\n",
-      "        # Set uniform and attribute\n",
-      "        self.program['a_color'] = gloo.VertexBuffer(v_color)\n",
-      "        self.program['a_position'] = gloo.VertexBuffer(v_position)\n",
-      "        self.program['a_size'] = gloo.VertexBuffer(v_size)\n",
-      "        gloo.set_state(clear_color='white', blend=True,\n",
-      "                       blend_func=('src_alpha', 'one_minus_src_alpha'))\n",
-      "\n",
-      "    def on_resize(self, event):\n",
-      "        gloo.set_viewport(0, 0, *event.size)\n",
-      "\n",
-      "    def on_draw(self, event):\n",
-      "        gloo.clear(color=True, depth=True)\n",
-      "        self.program.draw('points')\n",
-      "        self.update()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "p = Points()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "p.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
\ No newline at end of file
diff --git a/examples/ipynb/display_shape.ipynb b/examples/ipynb/display_shape.ipynb
deleted file mode 100644
index c8bbab4..0000000
--- a/examples/ipynb/display_shape.ipynb
+++ /dev/null
@@ -1,116 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:b48aa6253c1acca2536eafa8ee7779a21d80867ee71627c50a0dab733b2aa872"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "from vispy import gloo\n",
-      "from vispy import app, use\n",
-      "import numpy as np"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "use('ipynb_static')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "# Create vertices\n",
-      "vPosition = np.array([[-0.8, -0.8, 0.0], [+0.7, -0.7, 0.0],\n",
-      "                      [-0.7, +0.7, 0.0], [+0.8, +0.8, 0.0, ]], np.float32)\n",
-      "\n",
-      "\n",
-      "VERT_SHADER = \"\"\" // simple vertex shader\n",
-      "attribute vec3 a_position;\n",
-      "void main (void) {\n",
-      "    gl_Position = vec4(a_position, 1.0);\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "FRAG_SHADER = \"\"\" // simple fragment shader\n",
-      "uniform vec4 u_color;\n",
-      "void main()\n",
-      "{\n",
-      "    gl_FragColor = u_color;\n",
-      "}\n",
-      "\"\"\""
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Canvas(app.Canvas):\n",
-      "\n",
-      "    def __init__(self, *args, **kwargs):\n",
-      "        app.Canvas.__init__(self, *args, **kwargs)\n",
-      "\n",
-      "        # Create program\n",
-      "        self._program = gloo.Program(VERT_SHADER, FRAG_SHADER)\n",
-      "\n",
-      "        # Set uniform and attribute\n",
-      "        self._program['u_color'] = 0.2, 1.0, 0.4, 1\n",
-      "        self._program['a_position'] = gloo.VertexBuffer(vPosition)\n",
-      "\n",
-      "    def on_initialize(self, event):\n",
-      "        gloo.set_clear_color('white')\n",
-      "\n",
-      "    def on_resize(self, event):\n",
-      "        width, height = event.size\n",
-      "        gloo.set_viewport(0, 0, width, height)\n",
-      "\n",
-      "    def on_draw(self, event):\n",
-      "        gloo.clear()\n",
-      "        self._program.draw('triangle_strip')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "c = Canvas(size=(500,500))"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "c.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
\ No newline at end of file
diff --git a/examples/ipynb/donut.ipynb b/examples/ipynb/donut.ipynb
deleted file mode 100644
index 0fa980c..0000000
--- a/examples/ipynb/donut.ipynb
+++ /dev/null
@@ -1,255 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:21071064517113790dd14e6e5d596ea2a51c648e6062c756b7e48e8cb533a731"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "import numpy as np\n",
-      "from vispy import gloo\n",
-      "from vispy import app, use\n",
-      "from vispy.util.transforms import perspective, translate, rotate"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "use('ipynb_vnc')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "# Create vertices\n",
-      "n, p = 50, 40\n",
-      "data = np.zeros(p * n, [('a_position', np.float32, 2),\n",
-      "                        ('a_bg_color', np.float32, 4),\n",
-      "                        ('a_fg_color', np.float32, 4),\n",
-      "                        ('a_size',     np.float32, 1)])\n",
-      "data['a_position'][:, 0] = np.resize(np.linspace(0, 2 * np.pi, n), p * n)\n",
-      "data['a_position'][:, 1] = np.repeat(np.linspace(0, 2 * np.pi, p), n)\n",
-      "data['a_bg_color'] = np.random.uniform(0.75, 1.00, (n * p, 4))\n",
-      "data['a_bg_color'][:, 3] = 1\n",
-      "data['a_fg_color'] = 0, 0, 0, 1\n",
-      "data['a_size'] = np.random.uniform(8, 8, n * p)\n",
-      "u_linewidth = 1.0\n",
-      "u_antialias = 1.0\n",
-      "u_size = 1\n",
-      "\n",
-      "\n",
-      "vert = \"\"\"\n",
-      "#version 120\n",
-      "\n",
-      "uniform mat4 u_model;\n",
-      "uniform mat4 u_view;\n",
-      "uniform mat4 u_projection;\n",
-      "uniform float u_linewidth;\n",
-      "uniform float u_antialias;\n",
-      "uniform float u_size;\n",
-      "uniform float u_clock;\n",
-      "\n",
-      "attribute vec2  a_position;\n",
-      "attribute vec4  a_fg_color;\n",
-      "attribute vec4  a_bg_color;\n",
-      "attribute float a_size;\n",
-      "\n",
-      "varying vec4 v_fg_color;\n",
-      "varying vec4 v_bg_color;\n",
-      "varying float v_size;\n",
-      "varying float v_linewidth;\n",
-      "varying float v_antialias;\n",
-      "\n",
-      "void main (void) {\n",
-      "    v_size = a_size * u_size;\n",
-      "    v_linewidth = u_linewidth;\n",
-      "    v_antialias = u_antialias;\n",
-      "    v_fg_color  = a_fg_color;\n",
-      "    v_bg_color  = a_bg_color;\n",
-      "\n",
-      "    float x0 = 0.5;\n",
-      "    float z0 = 0.0;\n",
-      "\n",
-      "    float theta = a_position.x + u_clock;\n",
-      "    float x1 = x0*cos(theta) + z0*sin(theta) - 1.0;\n",
-      "    float y1 = 0.0;\n",
-      "    float z1 = z0*cos(theta) - x0*sin(theta);\n",
-      "\n",
-      "    float phi = a_position.y;\n",
-      "    float x2 = x1*cos(phi) + y1*sin(phi);\n",
-      "    float y2 = y1*cos(phi) - x1*sin(phi);\n",
-      "    float z2 = z1;\n",
-      "\n",
-      "    gl_Position = u_projection * u_view * u_model * vec4(x2,y2,z2,1);\n",
-      "    gl_PointSize = v_size + 2*(v_linewidth + 1.5*v_antialias);\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "frag = \"\"\"\n",
-      "#version 120\n",
-      "\n",
-      "varying vec4 v_fg_color;\n",
-      "varying vec4 v_bg_color;\n",
-      "varying float v_size;\n",
-      "varying float v_linewidth;\n",
-      "varying float v_antialias;\n",
-      "void main()\n",
-      "{\n",
-      "    float size = v_size +2*(v_linewidth + 1.5*v_antialias);\n",
-      "    float t = v_linewidth/2.0-v_antialias;\n",
-      "    float r = length((gl_PointCoord.xy - vec2(0.5,0.5))*size) - v_size/2;\n",
-      "    float d = abs(r) - t;\n",
-      "    if( r > (v_linewidth/2.0+v_antialias))\n",
-      "    {\n",
-      "        discard;\n",
-      "    }\n",
-      "    else if( d < 0.0 )\n",
-      "    {\n",
-      "       gl_FragColor = v_fg_color;\n",
-      "    }\n",
-      "    else\n",
-      "    {\n",
-      "        float alpha = d/v_antialias;\n",
-      "        alpha = exp(-alpha*alpha);\n",
-      "        if (r > 0)\n",
-      "            gl_FragColor = vec4(v_fg_color.rgb, alpha*v_fg_color.a);\n",
-      "        else\n",
-      "            gl_FragColor = mix(v_bg_color, v_fg_color, alpha);\n",
-      "    }\n",
-      "}\n",
-      "\"\"\""
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Donut(app.Canvas):\n",
-      "\n",
-      "    def __init__(self, *args, **kwargs):\n",
-      "        app.Canvas.__init__(self, *args, **kwargs)\n",
-      "\n",
-      "        self.program = gloo.Program(vert, frag)\n",
-      "        self.view = np.eye(4, dtype=np.float32)\n",
-      "        self.model = np.eye(4, dtype=np.float32)\n",
-      "        self.projection = np.eye(4, dtype=np.float32)\n",
-      "        self.translate = 5\n",
-      "        translate(self.view, 0, 0, -self.translate)\n",
-      "\n",
-      "        self.program.bind(gloo.VertexBuffer(data))\n",
-      "        self.program['u_linewidth'] = u_linewidth\n",
-      "        self.program['u_antialias'] = u_antialias\n",
-      "        self.program['u_model'] = self.model\n",
-      "        self.program['u_view'] = self.view\n",
-      "        self.program['u_size'] = 5 / self.translate\n",
-      "\n",
-      "        self.theta = 0\n",
-      "        self.phi = 0\n",
-      "        self.clock = 0\n",
-      "        self.stop_rotation = False\n",
-      "        \n",
-      "        self._timer = app.Timer(1.0 / 60, connect=self.on_timer, start=True)\n",
-      "        \n",
-      "    def on_initialize(self, event):\n",
-      "        gloo.set_state('translucent', clear_color=(1, 1, 1, 1))\n",
-      "\n",
-      "    def on_key_press(self, event):\n",
-      "        self.stop_rotation = not self.stop_rotation\n",
-      "\n",
-      "    def on_timer(self, event):\n",
-      "        if not self.stop_rotation:\n",
-      "            self.theta += .5\n",
-      "            self.phi += .5\n",
-      "            self.model = np.eye(4, dtype=np.float32)\n",
-      "            rotate(self.model, self.theta, 0, 0, 1)\n",
-      "            rotate(self.model, self.phi, 0, 1, 0)\n",
-      "            self.program['u_model'] = self.model\n",
-      "        self.clock += np.pi / 1000\n",
-      "        self.program['u_clock'] = self.clock\n",
-      "        self.update()\n",
-      "        \n",
-      "    def on_resize(self, event):\n",
-      "        width, height = event.size\n",
-      "        gloo.set_viewport(0, 0, width, height)\n",
-      "        self.projection = perspective(45.0, width / float(height), 1.0, 1000.0)\n",
-      "        self.program['u_projection'] = self.projection\n",
-      "\n",
-      "    def on_mouse_wheel(self, event):\n",
-      "        self.translate += event.delta[1]\n",
-      "        self.translate = max(2, self.translate)\n",
-      "        self.view = np.eye(4, dtype=np.float32)\n",
-      "        translate(self.view, 0, 0, -self.translate)\n",
-      "\n",
-      "        self.program['u_view'] = self.view\n",
-      "        self.program['u_size'] = 5 / self.translate\n",
-      "\n",
-      "    def on_draw(self, event):\n",
-      "        gloo.clear()\n",
-      "        self.program.draw('points')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "d = Donut(size=(300,300))"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "d.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "d.size = (800,800) # Resize test"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "d.close()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
\ No newline at end of file
diff --git a/examples/ipynb/fireworks.ipynb b/examples/ipynb/fireworks.ipynb
deleted file mode 100644
index 2071238..0000000
--- a/examples/ipynb/fireworks.ipynb
+++ /dev/null
@@ -1,195 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:f90783d43a54d451c9c62e63c1b7b8915313de217ac60d5d7db7dafedb3109c8"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "import time\n",
-      "import numpy as np\n",
-      "from vispy import gloo\n",
-      "from vispy import app, use"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "use('ipynb_vnc')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "# Create a texture\n",
-      "radius = 32\n",
-      "im1 = np.random.normal(\n",
-      "    0.8, 0.3, (radius * 2 + 1, radius * 2 + 1)).astype(np.float32)\n",
-      "\n",
-      "# Mask it with a disk\n",
-      "L = np.linspace(-radius, radius, 2 * radius + 1)\n",
-      "(X, Y) = np.meshgrid(L, L)\n",
-      "im1 *= np.array((X ** 2 + Y ** 2) <= radius * radius, dtype='float32')\n",
-      "\n",
-      "# Set number of particles, you should be able to scale this to 100000\n",
-      "N = 10000\n",
-      "\n",
-      "# Create vertex data container\n",
-      "data = np.zeros(N, [('a_lifetime', np.float32, 1),\n",
-      "                    ('a_startPosition', np.float32, 3),\n",
-      "                    ('a_endPosition', np.float32, 3)])\n",
-      "\n",
-      "\n",
-      "VERT_SHADER = \"\"\"\n",
-      "#version 120\n",
-      "uniform float u_time;\n",
-      "uniform vec3 u_centerPosition;\n",
-      "attribute float a_lifetime;\n",
-      "attribute vec3 a_startPosition;\n",
-      "attribute vec3 a_endPosition;\n",
-      "varying float v_lifetime;\n",
-      "\n",
-      "void main () {\n",
-      "    if (u_time <= a_lifetime)\n",
-      "    {\n",
-      "        gl_Position.xyz = a_startPosition + (u_time * a_endPosition);\n",
-      "        gl_Position.xyz += u_centerPosition;\n",
-      "        gl_Position.y -= 1.0 * u_time * u_time;\n",
-      "        gl_Position.w = 1.0;\n",
-      "    }\n",
-      "    else\n",
-      "        gl_Position = vec4(-1000, -1000, 0, 0);\n",
-      "\n",
-      "    v_lifetime = 1.0 - (u_time / a_lifetime);\n",
-      "    v_lifetime = clamp(v_lifetime, 0.0, 1.0);\n",
-      "    gl_PointSize = (v_lifetime * v_lifetime) * 40.0;\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "FRAG_SHADER = \"\"\"\n",
-      "#version 120\n",
-      "\n",
-      "uniform sampler2D texture1;\n",
-      "uniform vec4 u_color;\n",
-      "varying float v_lifetime;\n",
-      "uniform sampler2D s_texture;\n",
-      "\n",
-      "void main()\n",
-      "{\n",
-      "    vec4 texColor;\n",
-      "    texColor = texture2D(s_texture, gl_PointCoord);\n",
-      "    gl_FragColor = vec4(u_color) * texColor;\n",
-      "    gl_FragColor.a *= v_lifetime;\n",
-      "}\n",
-      "\"\"\""
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Firework(app.Canvas):\n",
-      "    def __init__(self, *args, **kwargs):\n",
-      "        app.Canvas.__init__(self, *args, **kwargs)\n",
-      "\n",
-      "        # Create program\n",
-      "        self._program = gloo.Program(VERT_SHADER, FRAG_SHADER)\n",
-      "        self._program.bind(gloo.VertexBuffer(data))\n",
-      "        self._program['s_texture'] = gloo.Texture2D(im1)\n",
-      "        self.explosion((0, 0))\n",
-      "\n",
-      "    def on_initialize(self, event):\n",
-      "        # Enable blending\n",
-      "        gloo.set_state(blend=True, clear_color=(0, 0, 0, 1),\n",
-      "                       blend_func=('src_alpha', 'one'))\n",
-      "    \n",
-      "    def on_mouse_press(self, event):\n",
-      "        self.explosion(event.pos)\n",
-      "\n",
-      "    def on_draw(self, event):\n",
-      "        # Clear\n",
-      "        gloo.clear()\n",
-      "        # Draw\n",
-      "        self._program['u_time'] = time.time() - self._starttime\n",
-      "        self._program.draw('points')\n",
-      "        self.update()\n",
-      "\n",
-      "    def explosion(self, pos):\n",
-      "        # New centerpos\n",
-      "        # centerpos = np.random.uniform(-0.5, 0.5, (3,))\n",
-      "        centerpos = np.ndarray(3)\n",
-      "        centerpos[0] = float(pos[0])/250.0 - 1.0\n",
-      "        centerpos[1] = 1.0 - float(pos[1])/250.0\n",
-      "        centerpos[2] = 0\n",
-      "        self._program['u_centerPosition'] = centerpos\n",
-      "\n",
-      "        # New color, scale alpha with N\n",
-      "        alpha = 1.0 / N ** 0.08\n",
-      "        color = np.random.uniform(0.1, 0.9, (3,))\n",
-      "\n",
-      "        self._program['u_color'] = tuple(color) + (alpha,)\n",
-      "\n",
-      "        # Create new vertex data\n",
-      "        data['a_lifetime'] = np.random.normal(2.0, 0.5, (N,))\n",
-      "        data['a_startPosition'] = np.random.normal(0.0, 0.2, (N, 3))\n",
-      "        data['a_endPosition'] = np.random.normal(0.0, 1.2, (N, 3))\n",
-      "        \n",
-      "        self._starttime = time.time()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "f = Firework(size=(500, 500))"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "# Click for fireworks!\n",
-      "f.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "f.close()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
\ No newline at end of file
diff --git a/examples/ipynb/galaxy.ipynb b/examples/ipynb/galaxy.ipynb
deleted file mode 100644
index a1e1431..0000000
--- a/examples/ipynb/galaxy.ipynb
+++ /dev/null
@@ -1,238 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:7a19dca3e35b17582c088f2d8e18cd4ae2d5b985b3c7c45f9f81fbd27a1532e2"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "import numpy as np\n",
-      "\n",
-      "from vispy import gloo\n",
-      "from vispy import app, use\n",
-      "from vispy.util.transforms import perspective, translate, rotate"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "app.use_app('ipynb_vnc')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "def make_arm(n, angle):\n",
-      "    R = np.linspace(10, 450 + 50 * np.random.uniform(.5, 1.), n)\n",
-      "    R += 40 * np.random.normal(0, 2., n) * np.linspace(1, .1, n)\n",
-      "    T = angle + np.linspace(0, 2.5 * np.pi, n) + \\\n",
-      "        np.pi / 6 * np.random.normal(0, .5, n)\n",
-      "    S = 8 + 2 * np.abs(np.random.normal(0, 1, n))\n",
-      "    S *= np.linspace(1, .85, n)\n",
-      "    P = np.zeros((n, 3), dtype=np.float32)\n",
-      "    X, Y, Z = P[:, 0], P[:, 1], P[:, 2]\n",
-      "    X[...] = R * np.cos(T)\n",
-      "    Y[...] = R * np.sin(T) * 1.1\n",
-      "    D = np.sqrt(X * X + Y * Y)\n",
-      "    Z[...] = 8 * np.random.normal(0, 2 - D / 512., n)\n",
-      "    X += (D * np.random.uniform(0, 1, n) > 250) * \\\n",
-      "        (.05 * D * np.random.uniform(-1, 1, n))\n",
-      "    Y += (D * np.random.uniform(0, 1, n) > 250) * \\\n",
-      "        (.05 * D * np.random.uniform(-1, 1, n))\n",
-      "    Z += (D * np.random.uniform(0, 1, n) > 250) * \\\n",
-      "        (.05 * D * np.random.uniform(-1, 1, n))\n",
-      "    D = (D - D.min()) / (D.max() - D.min())\n",
-      "\n",
-      "    return P / 256, S / 2, D\n",
-      "p = 50000\n",
-      "n = 3 * p\n",
-      "\n",
-      "data = np.zeros(n, [('a_position', np.float32, 3),\n",
-      "                    ('a_size', np.float32, 1),\n",
-      "                    ('a_dist', np.float32, 1)])\n",
-      "for i in range(3):\n",
-      "    P, S, D = make_arm(p, i * 2 * np.pi / 3)\n",
-      "    data['a_dist'][(i + 0) * p:(i + 1) * p] = D\n",
-      "    data['a_position'][(i + 0) * p:(i + 1) * p] = P\n",
-      "    data['a_size'][(i + 0) * p:(i + 1) * p] = S\n",
-      "\n",
-      "\n",
-      "# Very simple colormap\n",
-      "cmap = np.array([[255, 124, 0], [255, 163, 76],\n",
-      "                 [255, 192, 130], [255, 214, 173],\n",
-      "                 [255, 232, 212], [246, 238, 237],\n",
-      "                 [237, 240, 253], [217, 228, 255],\n",
-      "                 [202, 219, 255], [191, 212, 255],\n",
-      "                 [182, 206, 255], [174, 202, 255],\n",
-      "                 [168, 198, 255], [162, 195, 255],\n",
-      "                 [158, 192, 255], [155, 189, 255],\n",
-      "                 [151, 187, 255], [148, 185, 255],\n",
-      "                 [145, 183, 255], [143, 182, 255],\n",
-      "                 [141, 181, 255], [140, 179, 255],\n",
-      "                 [139, 179, 255],\n",
-      "                 [137, 177, 255]], dtype=np.uint8).reshape(1, 24, 3)\n",
-      "\n",
-      "\n",
-      "VERT_SHADER = \"\"\"\n",
-      "#version 120\n",
-      "// Uniforms\n",
-      "// ------------------------------------\n",
-      "uniform mat4  u_model;\n",
-      "uniform mat4  u_view;\n",
-      "uniform mat4  u_projection;\n",
-      "uniform float u_size;\n",
-      "\n",
-      "\n",
-      "// Attributes\n",
-      "// ------------------------------------\n",
-      "attribute vec3  a_position;\n",
-      "attribute float a_size;\n",
-      "attribute float a_dist;\n",
-      "\n",
-      "// Varyings\n",
-      "// ------------------------------------\n",
-      "varying float v_size;\n",
-      "varying float v_dist;\n",
-      "\n",
-      "void main (void) {\n",
-      "    v_size  = a_size*u_size*.75;\n",
-      "    v_dist  = a_dist;\n",
-      "    gl_Position = u_projection * u_view * u_model * vec4(a_position,1.0);\n",
-      "    gl_PointSize = v_size;\n",
-      "}\n",
-      "\"\"\"\n",
-      "FRAG_SHADER = \"\"\"\n",
-      "#version 120\n",
-      "// Uniforms\n",
-      "// ------------------------------------\n",
-      "uniform sampler2D u_colormap;\n",
-      "\n",
-      "// Varyings\n",
-      "// ------------------------------------\n",
-      "varying float v_size;\n",
-      "varying float v_dist;\n",
-      "\n",
-      "// Main\n",
-      "// ------------------------------------\n",
-      "void main()\n",
-      "{\n",
-      "    float a = 2*(length(gl_PointCoord.xy - vec2(0.5,0.5)) / sqrt(2.0));\n",
-      "    vec3 color = texture2D(u_colormap, vec2(v_dist,.5)).rgb;\n",
-      "    gl_FragColor = vec4(color,(1-a)*.25);\n",
-      "}\n",
-      "\"\"\""
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Galaxy(app.Canvas):\n",
-      "    \n",
-      "    def __init__(self, *args, **kwargs):\n",
-      "        app.Canvas.__init__(self, *args, **kwargs)\n",
-      "        self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)\n",
-      "        self.view = np.eye(4, dtype=np.float32)\n",
-      "        self.model = np.eye(4, dtype=np.float32)\n",
-      "        self.projection = np.eye(4, dtype=np.float32)\n",
-      "        self.theta, self.phi = 0, 0\n",
-      "\n",
-      "        self.translate = 5\n",
-      "        translate(self.view, 0, 0, -self.translate)\n",
-      "\n",
-      "        self.program.bind(gloo.VertexBuffer(data))\n",
-      "        self.program['u_colormap'] = gloo.Texture2D(cmap)\n",
-      "        self.program['u_size'] = 5. / self.translate\n",
-      "        self.program['u_model'] = self.model\n",
-      "        self.program['u_view'] = self.view\n",
-      "        \n",
-      "        self._timer = app.Timer(1.0 / 60, connect=self.on_timer, start=True)\n",
-      "\n",
-      "    def on_initialize(self, event):\n",
-      "        gloo.set_state(depth_test=False, blend=True,\n",
-      "                       blend_func=('src_alpha', 'one'),\n",
-      "                       clear_color=(0, 0, 0, 1))\n",
-      "\n",
-      "    def on_timer(self, event):\n",
-      "        self.theta += .11\n",
-      "        self.phi += .13\n",
-      "        self.model = np.eye(4, dtype=np.float32)\n",
-      "        rotate(self.model, self.theta, 0, 0, 1)\n",
-      "        rotate(self.model, self.phi, 0, 1, 0)\n",
-      "        self.program['u_model'] = self.model\n",
-      "        self.update()\n",
-      "\n",
-      "    def on_resize(self, event):\n",
-      "        width, height = event.size\n",
-      "        gloo.set_viewport(0, 0, width, height)\n",
-      "        self.projection = perspective(45.0, width / float(height), 1.0, 1000.0)\n",
-      "        self.program['u_projection'] = self.projection\n",
-      "\n",
-      "    def on_mouse_wheel(self, event):\n",
-      "        self.translate += event.delta[1]\n",
-      "        self.translate = max(2, self.translate)\n",
-      "        self.view = np.eye(4, dtype=np.float32)\n",
-      "        translate(self.view, 0, 0, -self.translate)\n",
-      "        self.program['u_view'] = self.view\n",
-      "        self.program['u_size'] = 5 / self.translate\n",
-      "\n",
-      "    def on_draw(self, event):\n",
-      "        gloo.clear()\n",
-      "        self.program.draw('points')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "g = Galaxy()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "g.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "g.close()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
\ No newline at end of file
diff --git a/examples/ipynb/mandelbrot.ipynb b/examples/ipynb/mandelbrot.ipynb
deleted file mode 100644
index 187a591..0000000
--- a/examples/ipynb/mandelbrot.ipynb
+++ /dev/null
@@ -1,251 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:6f42d4861c051c27386729397f659e04aee2048c30ffad0239cc7c79f8cc9e50"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "from vispy import app, use, gloo"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "use('ipynb_vnc')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "vertex = \"\"\"\n",
-      "attribute vec2 position;\n",
-      "\n",
-      "void main()\n",
-      "{\n",
-      "    gl_Position = vec4(position, 0, 1.0);\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "fragment = \"\"\"\n",
-      "uniform vec2 resolution;\n",
-      "uniform vec2 center;\n",
-      "uniform float scale;\n",
-      "uniform int iter;\n",
-      "\n",
-      "// Jet color scheme\n",
-      "vec4 color_scheme(float x) {\n",
-      "    vec3 a, b;\n",
-      "    float c;\n",
-      "    if (x < 0.34) {\n",
-      "        a = vec3(0, 0, 0.5);\n",
-      "        b = vec3(0, 0.8, 0.95);\n",
-      "        c = (x - 0.0) / (0.34 - 0.0);\n",
-      "    } else if (x < 0.64) {\n",
-      "        a = vec3(0, 0.8, 0.95);\n",
-      "        b = vec3(0.85, 1, 0.04);\n",
-      "        c = (x - 0.34) / (0.64 - 0.34);\n",
-      "    } else if (x < 0.89) {\n",
-      "        a = vec3(0.85, 1, 0.04);\n",
-      "        b = vec3(0.96, 0.7, 0);\n",
-      "        c = (x - 0.64) / (0.89 - 0.64);\n",
-      "    } else {\n",
-      "        a = vec3(0.96, 0.7, 0);\n",
-      "        b = vec3(0.5, 0, 0);\n",
-      "        c = (x - 0.89) / (1.0 - 0.89);\n",
-      "    }\n",
-      "    return vec4(mix(a, b, c), 1.0);\n",
-      "}\n",
-      "\n",
-      "void main() {\n",
-      "    vec2 z, c;\n",
-      "\n",
-      "    // Recover coordinates from pixel coordinates\n",
-      "    c.x = (gl_FragCoord.x / resolution.x - 0.5) * scale + center.x;\n",
-      "    c.y = (gl_FragCoord.y / resolution.y - 0.5) * scale + center.y;\n",
-      "\n",
-      "    // Main Mandelbrot computation\n",
-      "    int i;\n",
-      "    z = c;\n",
-      "    for(i = 0; i < iter; i++) {\n",
-      "        float x = (z.x * z.x - z.y * z.y) + c.x;\n",
-      "        float y = (z.y * z.x + z.x * z.y) + c.y;\n",
-      "\n",
-      "        if((x * x + y * y) > 4.0) break;\n",
-      "        z.x = x;\n",
-      "        z.y = y;\n",
-      "    }\n",
-      "\n",
-      "    // Convert iterations to color\n",
-      "    float color = 1.0 - float(i) / float(iter);\n",
-      "    gl_FragColor = color_scheme(color);\n",
-      "\n",
-      "}\n",
-      "\"\"\""
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Mandelbrot(app.Canvas):\n",
-      "    def __init__(self, *args, **kwargs):\n",
-      "        app.Canvas.__init__(self, *args, **kwargs)\n",
-      "        self.program = gloo.Program(vertex, fragment)\n",
-      "\n",
-      "        # Draw a rectangle that takes up the whole screen. All of the work is\n",
-      "        # done in the shader.\n",
-      "        self.program[\"position\"] = [(-1, -1), (-1, 1), (1, 1),\n",
-      "                                    (-1, -1), (1, 1), (1, -1)]\n",
-      "\n",
-      "        self.scale = self.program[\"scale\"] = 3\n",
-      "        self.center = self.program[\"center\"] = [-0.5, 0]\n",
-      "        self.iterations = self.program[\"iter\"] = 300\n",
-      "        self.program['resolution'] = self.size\n",
-      "\n",
-      "        self.bounds = [-2, 2]\n",
-      "        self.min_scale = 0.00005\n",
-      "        self.max_scale = 4\n",
-      "        \n",
-      "    def on_draw(self, event):\n",
-      "        self.program.draw()\n",
-      "        self.update()\n",
-      "\n",
-      "    def on_resize(self, event):\n",
-      "        width, height = event.size\n",
-      "        gloo.set_viewport(0, 0, width, height)\n",
-      "        self.program['resolution'] = [width, height]\n",
-      "\n",
-      "    def on_mouse_move(self, event):\n",
-      "        \"\"\"Pan the view based on the change in mouse position.\"\"\"\n",
-      "        if event.is_dragging and event.button == 0:\n",
-      "            x0, y0 = event.last_event.pos[0], event.last_event.pos[1]\n",
-      "            x1, y1 = event.pos[0], event.pos[1]\n",
-      "            X0, Y0 = self.pixel_to_coords(float(x0), float(y0))\n",
-      "            X1, Y1 = self.pixel_to_coords(float(x1), float(y1))\n",
-      "            self.translate_center(X1 - X0, Y1 - Y0)\n",
-      "        \n",
-      "    def translate_center(self, dx, dy):\n",
-      "        \"\"\"Translates the center point, and keeps it in bounds.\"\"\"\n",
-      "        center = self.center\n",
-      "        center[0] -= dx\n",
-      "        center[1] -= dy\n",
-      "        center[0] = min(max(center[0], self.bounds[0]), self.bounds[1])\n",
-      "        center[1] = min(max(center[1], self.bounds[0]), self.bounds[1])\n",
-      "        self.program[\"center\"] = self.center = center\n",
-      "\n",
-      "    def pixel_to_coords(self, x, y):\n",
-      "        \"\"\"Convert pixel coordinates to Mandelbrot set coordinates.\"\"\"\n",
-      "        rx, ry = self.size\n",
-      "        nx = (x / rx - 0.5) * self.scale + self.center[0]\n",
-      "        ny = ((ry - y) / ry - 0.5) * self.scale + self.center[1]\n",
-      "        return [nx, ny]\n",
-      "\n",
-      "    def on_mouse_wheel(self, event):\n",
-      "        \"\"\"Use the mouse wheel to zoom.\"\"\"\n",
-      "        delta = event.delta[1]\n",
-      "        if delta > 0:  # Zoom in\n",
-      "            factor = 0.9\n",
-      "        elif delta < 0:  # Zoom out\n",
-      "            factor = 1 / 0.9\n",
-      "        for _ in range(int(abs(delta))):\n",
-      "            self.zoom(factor, event.pos)\n",
-      "        \n",
-      "    def on_key_press(self, event):\n",
-      "        \"\"\"Use A or Z to zoom in and out.\n",
-      "\n",
-      "        The mouse wheel can be used to zoom, but some people don't have mouse\n",
-      "        wheels :)\n",
-      "\n",
-      "        \"\"\"\n",
-      "        if event.text == 'A':\n",
-      "            self.zoom(0.9)\n",
-      "        elif event.text == 'Z':\n",
-      "            self.zoom(1/0.9)    \n",
-      "        \n",
-      "    def zoom(self, factor, mouse_coords=None):\n",
-      "        \"\"\"Factors less than zero zoom in, and greater than zero zoom out.\n",
-      "\n",
-      "        If mouse_coords is given, the point under the mouse stays stationary\n",
-      "        while zooming. mouse_coords should come from MouseEvent.pos.\n",
-      "\n",
-      "        \"\"\"\n",
-      "        if mouse_coords:  # Record the position of the mouse\n",
-      "            x, y = float(mouse_coords[0]), float(mouse_coords[1])\n",
-      "            x0, y0 = self.pixel_to_coords(x, y)\n",
-      "\n",
-      "        self.scale *= factor\n",
-      "        self.scale = max(min(self.scale, self.max_scale), self.min_scale)\n",
-      "        self.program[\"scale\"] = self.scale\n",
-      "\n",
-      "        if mouse_coords:  # Translate so the mouse point is stationary\n",
-      "            x1, y1 = self.pixel_to_coords(x, y)\n",
-      "            self.translate_center(x1 - x0, y1 - y0)\n"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "m = Mandelbrot(size=(800, 600))"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "m.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "# Resize example\n",
-      "m.size = (400, 300)"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "m.close()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
\ No newline at end of file
diff --git a/examples/ipynb/post_processing.ipynb b/examples/ipynb/post_processing.ipynb
deleted file mode 100644
index 9ec9c1e..0000000
--- a/examples/ipynb/post_processing.ipynb
+++ /dev/null
@@ -1,228 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:6863b118647effae2133dbddd0370cb718493d0b8c580ab031e464b441de0e7f"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "import numpy as np\n",
-      "from vispy import app, use\n",
-      "\n",
-      "from vispy.geometry import create_cube\n",
-      "from vispy.util.transforms import perspective, translate, rotate\n",
-      "from vispy.gloo import (Program, VertexBuffer, IndexBuffer, Texture2D, clear,\n",
-      "                        FrameBuffer, DepthBuffer, set_viewport, set_state)"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "use('ipynb_vnc')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "cube_vertex = \"\"\"\n",
-      "uniform mat4 model;\n",
-      "uniform mat4 view;\n",
-      "uniform mat4 projection;\n",
-      "attribute vec3 position;\n",
-      "attribute vec2 texcoord;\n",
-      "attribute vec3 normal;\n",
-      "attribute vec4 color;\n",
-      "varying vec2 v_texcoord;\n",
-      "void main()\n",
-      "{\n",
-      "    gl_Position = projection * view * model * vec4(position,1.0);\n",
-      "    v_texcoord = texcoord;\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "cube_fragment = \"\"\"\n",
-      "uniform sampler2D texture;\n",
-      "varying vec2 v_texcoord;\n",
-      "void main()\n",
-      "{\n",
-      "    float r = texture2D(texture, v_texcoord).r;\n",
-      "    gl_FragColor = vec4(r,r,r,1);\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "quad_vertex = \"\"\"\n",
-      "attribute vec2 position;\n",
-      "attribute vec2 texcoord;\n",
-      "varying vec2 v_texcoord;\n",
-      "void main()\n",
-      "{\n",
-      "    gl_Position = vec4(position, 0.0, 1.0);\n",
-      "    v_texcoord = texcoord;\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "quad_fragment = \"\"\"\n",
-      "uniform sampler2D texture;\n",
-      "varying vec2 v_texcoord;\n",
-      "void main()\n",
-      "{\n",
-      "    vec2 d = 5.0 * vec2(sin(v_texcoord.y*50.0),0)/512.0;\n",
-      "\n",
-      "    // Inverse video\n",
-      "    if( v_texcoord.x > 0.5 ) {\n",
-      "        gl_FragColor.rgb = 1.0-texture2D(texture, v_texcoord+d).rgb;\n",
-      "    } else {\n",
-      "        gl_FragColor = texture2D(texture, v_texcoord);\n",
-      "    }\n",
-      "}\n",
-      "\"\"\""
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "def checkerboard(grid_num=8, grid_size=32):\n",
-      "    row_even = grid_num // 2 * [0, 1]\n",
-      "    row_odd = grid_num // 2 * [1, 0]\n",
-      "    Z = np.row_stack(grid_num // 2 * (row_even, row_odd)).astype(np.uint8)\n",
-      "    return 255 * Z.repeat(grid_size, axis=0).repeat(grid_size, axis=1)"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Canvas(app.Canvas):\n",
-      "\n",
-      "    def __init__(self):\n",
-      "        app.Canvas.__init__(self, title='Framebuffer post-processing',\n",
-      "                            size=(512, 512))\n",
-      "        self._timer = app.Timer(1.0 / 60, connect=self.on_timer, start=True)\n",
-      "\n",
-      "    def on_initialize(self, event):\n",
-      "        # Build cube data\n",
-      "        # --------------------------------------\n",
-      "        vertices, indices, _ = create_cube()\n",
-      "        vertices = VertexBuffer(vertices)\n",
-      "        self.indices = IndexBuffer(indices)\n",
-      "\n",
-      "        # Build program\n",
-      "        # --------------------------------------\n",
-      "        view = np.eye(4, dtype=np.float32)\n",
-      "        model = np.eye(4, dtype=np.float32)\n",
-      "        translate(view, 0, 0, -7)\n",
-      "        self.phi, self.theta = 60, 20\n",
-      "        rotate(model, self.theta, 0, 0, 1)\n",
-      "        rotate(model, self.phi, 0, 1, 0)\n",
-      "\n",
-      "        self.cube = Program(cube_vertex, cube_fragment)\n",
-      "        self.cube.bind(vertices)\n",
-      "        self.cube[\"texture\"] = checkerboard()\n",
-      "        self.cube[\"texture\"].interpolation = 'linear'\n",
-      "        self.cube['model'] = model\n",
-      "        self.cube['view'] = view\n",
-      "\n",
-      "        depth = DepthBuffer((512, 512))\n",
-      "        color = Texture2D(shape=(512, 512, 3), dtype=np.dtype(np.float32))\n",
-      "        self.framebuffer = FrameBuffer(color=color, depth=depth)\n",
-      "\n",
-      "        self.quad = Program(quad_vertex, quad_fragment, count=4)\n",
-      "        self.quad['texcoord'] = [(0, 0), (0, 1), (1, 0), (1, 1)]\n",
-      "        self.quad['position'] = [(-1, -1), (-1, +1), (+1, -1), (+1, +1)]\n",
-      "        self.quad['texture'] = color\n",
-      "        self.quad[\"texture\"].interpolation = 'linear'\n",
-      "\n",
-      "        # OpenGL and Timer initalization\n",
-      "        # --------------------------------------\n",
-      "        set_state(clear_color=(.3, .3, .35, 1), depth_test=True)\n",
-      "        self._set_projection(self.size)\n",
-      "\n",
-      "    def on_draw(self, event):\n",
-      "        self.framebuffer.activate()\n",
-      "        set_viewport(0, 0, 512, 512)\n",
-      "        clear(color=True, depth=True)\n",
-      "        set_state(depth_test=True)\n",
-      "        self.cube.draw('triangles', self.indices)\n",
-      "        self.framebuffer.deactivate()\n",
-      "        clear(color=True)\n",
-      "        set_state(depth_test=False)\n",
-      "        self.quad.draw('triangle_strip')\n",
-      "\n",
-      "    def on_resize(self, event):\n",
-      "        self._set_projection(event.size)\n",
-      "\n",
-      "    def _set_projection(self, size):\n",
-      "        width, height = size\n",
-      "        set_viewport(0, 0, width, height)\n",
-      "        projection = perspective(30.0, width / float(height), 2.0, 10.0)\n",
-      "        self.cube['projection'] = projection\n",
-      "\n",
-      "    def on_timer(self, event):\n",
-      "        self.theta += .5\n",
-      "        self.phi += .5\n",
-      "        model = np.eye(4, dtype=np.float32)\n",
-      "        rotate(model, self.theta, 0, 0, 1)\n",
-      "        rotate(model, self.phi, 0, 1, 0)\n",
-      "        self.cube['model'] = model\n",
-      "        self.update()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "c = Canvas()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "c.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "c.close()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
diff --git a/examples/ipynb/rain.ipynb b/examples/ipynb/rain.ipynb
deleted file mode 100644
index fa85e75..0000000
--- a/examples/ipynb/rain.ipynb
+++ /dev/null
@@ -1,202 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:6520e892443ed78df97922e18a8dfebc72d4c0408ead6302ef3e63b4bc194eee"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "import numpy as np\n",
-      "\n",
-      "from vispy import gloo, app, use\n",
-      "from vispy.gloo import Program, VertexBuffer\n",
-      "from vispy.util.transforms import ortho"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "use('ipynb_vnc')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "vertex = \"\"\"\n",
-      "#version 120\n",
-      "\n",
-      "uniform mat4  u_model;\n",
-      "uniform mat4  u_view;\n",
-      "uniform mat4  u_projection;\n",
-      "uniform float u_linewidth;\n",
-      "uniform float u_antialias;\n",
-      "\n",
-      "attribute vec3  a_position;\n",
-      "attribute vec4  a_fg_color;\n",
-      "attribute float a_size;\n",
-      "\n",
-      "varying vec4  v_fg_color;\n",
-      "varying float v_size;\n",
-      "\n",
-      "void main (void)\n",
-      "{\n",
-      "    v_size = a_size;\n",
-      "    v_fg_color = a_fg_color;\n",
-      "    if( a_fg_color.a > 0.0)\n",
-      "    {\n",
-      "        gl_Position = u_projection * u_view * u_model * vec4(a_position,1.0);\n",
-      "        gl_PointSize = v_size + u_linewidth + 2*1.5*u_antialias;\n",
-      "    }\n",
-      "    else\n",
-      "    {\n",
-      "        gl_Position = u_projection * u_view * u_model * vec4(-1,-1,0,1);\n",
-      "        gl_PointSize = 0.0;\n",
-      "    }\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "fragment = \"\"\"\n",
-      "#version 120\n",
-      "\n",
-      "uniform float u_linewidth;\n",
-      "uniform float u_antialias;\n",
-      "varying vec4  v_fg_color;\n",
-      "varying vec4  v_bg_color;\n",
-      "varying float v_size;\n",
-      "float disc(vec2 P, float size)\n",
-      "{\n",
-      "    return length((P.xy - vec2(0.5,0.5))*size);\n",
-      "}\n",
-      "void main()\n",
-      "{\n",
-      "    if( v_fg_color.a <= 0.0)\n",
-      "        discard;\n",
-      "    float actual_size = v_size + u_linewidth + 2*1.5*u_antialias;\n",
-      "    float t = u_linewidth/2.0 - u_antialias;\n",
-      "    float r = disc(gl_PointCoord, actual_size);\n",
-      "    float d = abs(r - v_size/2.0) - t;\n",
-      "    if( d < 0.0 )\n",
-      "    {\n",
-      "         gl_FragColor = v_fg_color;\n",
-      "    }\n",
-      "    else if( abs(d) > 2.5*u_antialias )\n",
-      "    {\n",
-      "         discard;\n",
-      "    }\n",
-      "    else\n",
-      "    {\n",
-      "        d /= u_antialias;\n",
-      "        gl_FragColor = vec4(v_fg_color.rgb, exp(-d*d)*v_fg_color.a);\n",
-      "    }\n",
-      "}\n",
-      "\"\"\""
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Rain(app.Canvas):\n",
-      "    def __init__(self):\n",
-      "        app.Canvas.__init__(self, title='Rain [Move mouse]',\n",
-      "                            size=(512, 512))\n",
-      "        self._timer = app.Timer(1.0 / 60, connect=self.on_timer, start=True)\n",
-      "\n",
-      "    def on_initialize(self, event):\n",
-      "        # Build data\n",
-      "        # --------------------------------------\n",
-      "        n = 500\n",
-      "        self.data = np.zeros(n, [('a_position', np.float32, 2),\n",
-      "                                 ('a_fg_color', np.float32, 4),\n",
-      "                                 ('a_size',     np.float32, 1)])\n",
-      "        self.index = 0\n",
-      "        self.program = Program(vertex, fragment)\n",
-      "        self.vdata = VertexBuffer(self.data)\n",
-      "        self.program.bind(self.vdata)\n",
-      "        self.program['u_antialias'] = 1.00\n",
-      "        self.program['u_linewidth'] = 1.00\n",
-      "        self.program['u_model'] = np.eye(4, dtype=np.float32)\n",
-      "        self.program['u_view'] = np.eye(4, dtype=np.float32)\n",
-      "        gloo.set_clear_color('white')\n",
-      "        gloo.set_state(blend=True,\n",
-      "                       blend_func=('src_alpha', 'one_minus_src_alpha'))\n",
-      "\n",
-      "    def on_draw(self, event):\n",
-      "        gloo.clear()\n",
-      "        self.program.draw('points')\n",
-      "        self.update()\n",
-      "\n",
-      "    def on_resize(self, event):\n",
-      "        gloo.set_viewport(0, 0, *event.size)\n",
-      "        projection = ortho(0, event.size[0], 0, event.size[1], -1, +1)\n",
-      "        self.program['u_projection'] = projection\n",
-      "\n",
-      "    def on_timer(self, event):\n",
-      "        self.data['a_fg_color'][..., 3] -= 0.01\n",
-      "        self.data['a_size'] += 1.0\n",
-      "        self.vdata.set_data(self.data)\n",
-      "\n",
-      "    def on_mouse_move(self, event):\n",
-      "        x, y = event.pos\n",
-      "        h = gloo.get_parameter('viewport')[3]\n",
-      "        self.data['a_position'][self.index] = x, h - y\n",
-      "        self.data['a_size'][self.index] = 5\n",
-      "        self.data['a_fg_color'][self.index] = 0, 0, 0, 1\n",
-      "        self.index = (self.index + 1) % 500"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "r = Rain()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "r.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "r.close()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
\ No newline at end of file
diff --git a/examples/ipynb/spacy.ipynb b/examples/ipynb/spacy.ipynb
deleted file mode 100644
index ab41f82..0000000
--- a/examples/ipynb/spacy.ipynb
+++ /dev/null
@@ -1,220 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:7339f421d784141cfef90fc1d87b21e83beb837c211535f685cb9c8b7d0d2d17"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "import time\n",
-      "\n",
-      "import numpy as np\n",
-      "\n",
-      "from vispy import gloo\n",
-      "from vispy import app, use\n",
-      "from vispy.util.transforms import perspective"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "use('ipynb_vnc')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "vertex = \"\"\"\n",
-      "#version 120\n",
-      "\n",
-      "uniform mat4 u_model;\n",
-      "uniform mat4 u_view;\n",
-      "uniform mat4 u_projection;\n",
-      "uniform float u_time_offset;\n",
-      "\n",
-      "attribute vec3  a_position;\n",
-      "attribute float a_offset;\n",
-      "\n",
-      "varying float v_pointsize;\n",
-      "\n",
-      "void main (void) {\n",
-      "   \n",
-      "    vec3 pos = a_position;\n",
-      "    pos.z = pos.z - a_offset - u_time_offset;\n",
-      "    vec4 v_eye_position = u_view * u_model * vec4(pos, 1.0);\n",
-      "    gl_Position = u_projection * v_eye_position;\n",
-      "\n",
-      "    // stackoverflow.com/questions/8608844/...\n",
-      "    //  ... resizing-point-sprites-based-on-distance-from-the-camera\n",
-      "    float radius = 1;\n",
-      "    vec4 corner = vec4(radius, radius, v_eye_position.z, v_eye_position.w);\n",
-      "    vec4  proj_corner = u_projection * corner;\n",
-      "    gl_PointSize = 100.0 * proj_corner.x / proj_corner.w;\n",
-      "    v_pointsize = gl_PointSize;\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "fragment = \"\"\"\n",
-      "#version 120\n",
-      "varying float v_pointsize;\n",
-      "void main()\n",
-      "{\n",
-      "    float x = 2.0*gl_PointCoord.x - 1.0;\n",
-      "    float y = 2.0*gl_PointCoord.y - 1.0;\n",
-      "    float a = 0.9 - (x*x + y*y);\n",
-      "    a = a * min(1.0, v_pointsize/1.5);\n",
-      "    gl_FragColor = vec4(1.0, 1.0, 1.0, a);\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "N = 10000  # Number of stars \n",
-      "SIZE = 100\n",
-      "SPEED = 4.0  # time in seconds to go through one block\n",
-      "NBLOCKS = 10\n"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Space(app.Canvas):\n",
-      "\n",
-      "    def __init__(self, *args, **kwargs):\n",
-      "        app.Canvas.__init__(self, *args, **kwargs)\n",
-      "        \n",
-      "        self.program = gloo.Program(vertex, fragment)\n",
-      "        self.view = np.eye(4, dtype=np.float32)\n",
-      "        self.model = np.eye(4, dtype=np.float32)\n",
-      "        self.projection = np.eye(4, dtype=np.float32)\n",
-      "                \n",
-      "        # Set uniforms (some are set later)\n",
-      "        self.program['u_model'] = self.model\n",
-      "        self.program['u_view'] = self.view\n",
-      "        \n",
-      "        # Set attributes\n",
-      "        self.program['a_position'] = np.zeros((N, 3), np.float32)\n",
-      "        self.program['a_offset'] = np.zeros((N,), np.float32)\n",
-      "        \n",
-      "        # Init\n",
-      "        self._timeout = 0\n",
-      "        self._active_block = 0\n",
-      "        for i in range(NBLOCKS):\n",
-      "            self._generate_stars()\n",
-      "        self._timeout = time.time() + SPEED\n",
-      "        \n",
-      "        self._timer = app.Timer(1.0 / 60, connect=self.on_timer, start=True)\n",
-      "    \n",
-      "    def on_initialize(self, event):\n",
-      "        gloo.set_state(clear_color=(0.0, 0.0, 0.0, 0.0), depth_test=False,\n",
-      "                       blend=True, blend_equation='func_add',\n",
-      "                       blend_func=('src_alpha', 'one_minus_src_alpha'))\n",
-      "        \n",
-      "    def on_timer(self, event):\n",
-      "        self.update()\n",
-      "        \n",
-      "    def on_resize(self, event):\n",
-      "        width, height = event.size\n",
-      "        gloo.set_viewport(0, 0, width, height)\n",
-      "        far = SIZE*(NBLOCKS-2)\n",
-      "        self.projection = perspective(25.0, width / float(height), 1.0, far)\n",
-      "        self.program['u_projection'] = self.projection\n",
-      "\n",
-      "    def on_draw(self, event):\n",
-      "        # Set time offset. Factor runs from 1 to 0\n",
-      "        # the time offset goes from 0 to size\n",
-      "        factor = (self._timeout - time.time()) / SPEED\n",
-      "        self.program['u_time_offset'] = -(1-factor) * SIZE\n",
-      "        \n",
-      "        # Draw\n",
-      "        gloo.clear(color=(0., 0., 0., 1.))\n",
-      "        self.program.draw('points')\n",
-      "        \n",
-      "        # Build new starts if the first block is fully behind us\n",
-      "        if factor < 0:\n",
-      "            self._generate_stars()\n",
-      "    \n",
-      "    def _generate_stars(self):\n",
-      "        \n",
-      "        # Get number of stars in each block\n",
-      "        blocksize = N // NBLOCKS\n",
-      "        \n",
-      "        # Update active block\n",
-      "        self._active_block += 1\n",
-      "        if self._active_block >= NBLOCKS:\n",
-      "            self._active_block = 0\n",
-      "        \n",
-      "        # Create new position data for the active block\n",
-      "        pos = np.zeros((blocksize, 3), 'float32') \n",
-      "        pos[:, :2] = np.random.normal(0.0, SIZE/2, (blocksize, 2))  # x-y\n",
-      "        pos[:, 2] = np.random.uniform(0, SIZE, (blocksize,))  # z\n",
-      "        start_index = self._active_block * blocksize\n",
-      "        self.program['a_position'].set_data(pos, offset=start_index) \n",
-      "        \n",
-      "        # Set offsets - active block gets offset 0\n",
-      "        for i in range(NBLOCKS):\n",
-      "            val = i - self._active_block\n",
-      "            if val < 0:\n",
-      "                val += NBLOCKS\n",
-      "            values = np.ones((blocksize, 1), 'float32') * val * SIZE\n",
-      "            start_index = i*blocksize\n",
-      "            self.program['a_offset'].set_data(values, offset=start_index) \n",
-      "        \n",
-      "        # Reset timer\n",
-      "        self._timeout += SPEED"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "s = Space()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "s.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "s.close()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
\ No newline at end of file
diff --git a/examples/ipynb/voronoi.ipynb b/examples/ipynb/voronoi.ipynb
deleted file mode 100644
index 2920fb2..0000000
--- a/examples/ipynb/voronoi.ipynb
+++ /dev/null
@@ -1,172 +0,0 @@
-{
- "metadata": {
-  "name": "",
-  "signature": "sha256:c3963f2cba5f56be4cccbc9e47bc9593f1b72934b7298603c51400dfb528661d"
- },
- "nbformat": 3,
- "nbformat_minor": 0,
- "worksheets": [
-  {
-   "cells": [
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "import numpy as np\n",
-      "\n",
-      "from vispy import app, use\n",
-      "from vispy import gloo"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "use('ipynb_vnc')"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "# Voronoi shaders.\n",
-      "VS_voronoi = \"\"\"\n",
-      "attribute vec2 a_position;\n",
-      "\n",
-      "void main() {\n",
-      "    gl_Position = vec4(a_position, 0., 1.);\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "FS_voronoi = \"\"\"\n",
-      "uniform vec2 u_seeds[32];\n",
-      "uniform vec3 u_colors[32];\n",
-      "uniform vec2 u_screen;\n",
-      "\n",
-      "void main() {\n",
-      "    float dist = distance(u_screen * u_seeds[0], gl_FragCoord.xy);\n",
-      "    vec3 color = u_colors[0];\n",
-      "    for (int i = 1; i < 32; i++) {\n",
-      "        float current = distance(u_screen * u_seeds[i], gl_FragCoord.xy);\n",
-      "        if (current < dist) {\n",
-      "            color = u_colors[i];\n",
-      "            dist = current;\n",
-      "        }\n",
-      "    }\n",
-      "    gl_FragColor = vec4(color, 1.0);\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "\n",
-      "# Seed point shaders.\n",
-      "VS_seeds = \"\"\"\n",
-      "attribute vec2 a_position;\n",
-      "\n",
-      "void main() {\n",
-      "    gl_Position = vec4(2. * a_position - 1., 0., 1.);\n",
-      "    gl_PointSize = 10.;\n",
-      "}\n",
-      "\"\"\"\n",
-      "\n",
-      "FS_seeds = \"\"\"\n",
-      "varying vec3 v_color;\n",
-      "void main() {\n",
-      "    gl_FragColor = vec4(1., 1., 1., 1.);\n",
-      "}\n",
-      "\"\"\""
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "class Voronoi(app.Canvas):\n",
-      "    def __init__(self):\n",
-      "        app.Canvas.__init__(self, size=(600, 600))\n",
-      "        \n",
-      "        self.seeds = np.random.uniform(0, 1,\n",
-      "                                       size=(32, 2)).astype(np.float32)\n",
-      "        self.colors = np.random.uniform(0.3, 0.8, \n",
-      "                                        size=(32, 3)).astype(np.float32)\n",
-      "        \n",
-      "        # Set Voronoi program.\n",
-      "        self.program_v = gloo.Program(VS_voronoi, FS_voronoi)\n",
-      "        self.program_v['a_position'] = [(-1, -1), (-1, +1), (+1, -1), (+1, +1)]\n",
-      "        # HACK: work-around a bug related to uniform arrays until \n",
-      "        # issue #345 is solved.\n",
-      "        for i in range(32):\n",
-      "            self.program_v['u_seeds[%d]' % i] = self.seeds[i, :]\n",
-      "            self.program_v['u_colors[%d]' % i] = self.colors[i, :]\n",
-      "            \n",
-      "        # Set seed points program.\n",
-      "        self.program_s = gloo.Program(VS_seeds, FS_seeds)\n",
-      "        self.program_s['a_position'] = self.seeds\n",
-      "\n",
-      "    def on_draw(self, event):\n",
-      "        gloo.clear()\n",
-      "        self.program_v.draw('triangle_strip')\n",
-      "        self.program_s.draw('points')\n",
-      "\n",
-      "    def on_resize(self, event):\n",
-      "        self.width, self.height = event.size\n",
-      "        gloo.set_viewport(0, 0, self.width, self.height)\n",
-      "        self.program_v['u_screen'] = (self.width, self.height)\n",
-      "        \n",
-      "    def on_mouse_move(self, event):\n",
-      "        x, y = event.pos\n",
-      "        x, y = x/float(self.width), 1-y/float(self.height)\n",
-      "        self.program_v['u_seeds[0]'] = x, y\n",
-      "        # TODO: just update the first line in the VBO instead of uploading the\n",
-      "        # whole array of seed points.\n",
-      "        self.seeds[0, :] = x, y\n",
-      "        self.program_s['a_position'].set_data(self.seeds)\n",
-      "        self.update()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "v = Voronoi()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "v.show()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    },
-    {
-     "cell_type": "code",
-     "collapsed": false,
-     "input": [
-      "v.close()"
-     ],
-     "language": "python",
-     "metadata": {},
-     "outputs": []
-    }
-   ],
-   "metadata": {}
-  }
- ]
-}
\ No newline at end of file
diff --git a/examples/ipynb/webgl_example_1.ipynb b/examples/ipynb/webgl_example_1.ipynb
new file mode 100644
index 0000000..d1c0575
--- /dev/null
+++ b/examples/ipynb/webgl_example_1.ipynb
@@ -0,0 +1,211 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# WebGL backend demo"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "**WARNING: the WebGL backend requires IPython 3.0 (or the master branch of IPython until 3.0 is released)**"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import vispy\n",
+    "import vispy.gloo as gloo\n",
+    "from vispy import app\n",
+    "from vispy.util.transforms import perspective, translate, rotate\n",
+    "app.use_app('ipynb_webgl');"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "n = 100\n",
+    "a_position = np.random.uniform(-1, 1, (n, 3)).astype(np.float32)\n",
+    "a_id = np.random.randint(0, 30, (n, 1))\n",
+    "a_id = np.sort(a_id, axis=0).astype(np.float32)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "VERT_SHADER = \"\"\"\n",
+    "uniform mat4 u_model;\n",
+    "uniform mat4 u_view;\n",
+    "uniform mat4 u_projection;\n",
+    "attribute vec3 a_position;\n",
+    "attribute float a_id;\n",
+    "varying float v_id;\n",
+    "void main (void) {\n",
+    "    v_id = a_id;\n",
+    "    gl_Position = u_projection * u_view * u_model * vec4(a_position,1.0);\n",
+    "}\n",
+    "\"\"\"\n",
+    "\n",
+    "FRAG_SHADER = \"\"\"\n",
+    "varying float v_id;\n",
+    "void main()\n",
+    "{\n",
+    "    float f = fract(v_id);\n",
+    "    // The second useless test is needed on OSX 10.8 (fuck)\n",
+    "    if( (f > 0.0001) && (f < .9999) )\n",
+    "        discard;\n",
+    "    else\n",
+    "        gl_FragColor = vec4(0,0,0,1);\n",
+    "}\n",
+    "\"\"\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "class Canvas(app.Canvas):\n",
+    "\n",
+    "    # ---------------------------------\n",
+    "    def __init__(self, size=None):\n",
+    "        app.Canvas.__init__(self, keys='interactive', size=size)\n",
+    "\n",
+    "        self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)\n",
+    "\n",
+    "        # Set uniform and attribute\n",
+    "        self.program['a_id'] = gloo.VertexBuffer(a_id)\n",
+    "        self.program['a_position'] = gloo.VertexBuffer(a_position)\n",
+    "\n",
+    "        self.translate = 5\n",
+    "        self.view = translate((0, 0, -self.translate), dtype=np.float32)\n",
+    "        self.model = np.eye(4, dtype=np.float32)\n",
+    "\n",
+    "        gloo.set_viewport(0, 0, self.physical_size[0], self.physical_size[1])\n",
+    "        self.projection = perspective(45.0, self.size[0] /\n",
+    "                                      float(self.size[1]), 1.0, 1000.0)\n",
+    "        self.program['u_projection'] = self.projection\n",
+    "\n",
+    "        self.program['u_model'] = self.model\n",
+    "        self.program['u_view'] = self.view\n",
+    "\n",
+    "        self.theta = 0\n",
+    "        self.phi = 0\n",
+    "\n",
+    "        self.context.set_clear_color('white')\n",
+    "        self.context.set_state('translucent')\n",
+    "\n",
+    "        self.timer = app.Timer('auto', connect=self.on_timer, start=True)\n",
+    "\n",
+    "        self.show()\n",
+    "\n",
+    "    # ---------------------------------\n",
+    "    def on_key_press(self, event):\n",
+    "        if event.text == ' ':\n",
+    "            if self.timer.running:\n",
+    "                self.timer.stop()\n",
+    "            else:\n",
+    "                self.timer.start()\n",
+    "\n",
+    "    # ---------------------------------\n",
+    "    def on_timer(self, event):\n",
+    "        self.theta += .5\n",
+    "        self.phi += .5\n",
+    "        self.model = np.dot(rotate(self.theta, (0, 0, 1)),\n",
+    "                            rotate(self.phi, (0, 1, 0)))\n",
+    "        self.program['u_model'] = self.model\n",
+    "        self.update()\n",
+    "\n",
+    "    # ---------------------------------\n",
+    "    def on_resize(self, event):\n",
+    "        gloo.set_viewport(0, 0, event.physical_size[0], event.physical_size[1])\n",
+    "        self.projection = perspective(45.0, event.size[0] /\n",
+    "                                      float(event.size[1]), 1.0, 1000.0)\n",
+    "        self.program['u_projection'] = self.projection\n",
+    "\n",
+    "    # ---------------------------------\n",
+    "    def on_mouse_wheel(self, event):\n",
+    "        self.translate += event.delta[1]\n",
+    "        self.translate = max(2, self.translate)\n",
+    "        self.view = translate((0, 0, -self.translate))\n",
+    "        self.program['u_view'] = self.view\n",
+    "        self.update()\n",
+    "\n",
+    "    # ---------------------------------\n",
+    "    def on_draw(self, event):\n",
+    "        self.context.clear()\n",
+    "        self.program.draw('line_strip')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "c = Canvas(size=(300, 300))\n",
+    "#or\n",
+    "#from vispy.app.backends.ipython import VispyWidget\n",
+    "#w = VispyWidget()\n",
+    "#w.set_canvas(c)\n",
+    "#w"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "#c.timer.stop()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.4.3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/examples/ipynb/webgl_example_2.ipynb b/examples/ipynb/webgl_example_2.ipynb
new file mode 100644
index 0000000..3f4512f
--- /dev/null
+++ b/examples/ipynb/webgl_example_2.ipynb
@@ -0,0 +1,270 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# WebGL backend demo: molecular viewer in the notebook"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "**WARNING: the WebGL backend requires IPython 3.0 (or the master branch of IPython until 3.0 is released)**"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "\n",
+    "from vispy import gloo\n",
+    "from vispy import app\n",
+    "from vispy.util.transforms import perspective, translate, rotate\n",
+    "from vispy.io import load_data_file\n",
+    "\n",
+    "app.use_app('ipynb_webgl')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "vertex = \"\"\"\n",
+    "uniform mat4 u_model;\n",
+    "uniform mat4 u_view;\n",
+    "uniform mat4 u_projection;\n",
+    "uniform vec3 u_light_position;\n",
+    "uniform vec3 u_light_spec_position;\n",
+    "\n",
+    "attribute vec3  a_position;\n",
+    "attribute vec3  a_color;\n",
+    "attribute float a_radius;\n",
+    "\n",
+    "varying vec3  v_color;\n",
+    "varying vec4  v_eye_position;\n",
+    "varying float v_radius;\n",
+    "varying vec3  v_light_direction;\n",
+    "\n",
+    "void main (void) {\n",
+    "    v_radius = a_radius;\n",
+    "    v_color = a_color;\n",
+    "\n",
+    "    v_eye_position = u_view * u_model * vec4(a_position,1.0);\n",
+    "    v_light_direction = normalize(u_light_position);\n",
+    "    float dist = length(v_eye_position.xyz);\n",
+    "\n",
+    "    gl_Position = u_projection * v_eye_position;\n",
+    "\n",
+    "    // stackoverflow.com/questions/8608844/...\n",
+    "    //  ... resizing-point-sprites-based-on-distance-from-the-camera\n",
+    "    vec4  proj_corner = u_projection * vec4(a_radius, a_radius, v_eye_position.z, v_eye_position.w);  // # noqa\n",
+    "    gl_PointSize = 512.0 * proj_corner.x / proj_corner.w;\n",
+    "}\n",
+    "\"\"\"\n",
+    "\n",
+    "fragment = \"\"\"\n",
+    "uniform mat4 u_model;\n",
+    "uniform mat4 u_view;\n",
+    "uniform mat4 u_projection;\n",
+    "uniform vec3 u_light_position;\n",
+    "uniform vec3 u_light_spec_position;\n",
+    "\n",
+    "varying vec3  v_color;\n",
+    "varying vec4  v_eye_position;\n",
+    "varying float v_radius;\n",
+    "varying vec3  v_light_direction;\n",
+    "void main()\n",
+    "{\n",
+    "    // r^2 = (x - x0)^2 + (y - y0)^2 + (z - z0)^2\n",
+    "    vec2 texcoord = gl_PointCoord* 2.0 - vec2(1.0);\n",
+    "    float x = texcoord.x;\n",
+    "    float y = texcoord.y;\n",
+    "    float d = 1.0 - x*x - y*y;\n",
+    "    if (d <= 0.0)\n",
+    "        discard;\n",
+    "\n",
+    "    float z = sqrt(d);\n",
+    "    vec4 pos = v_eye_position;\n",
+    "    pos.z += v_radius*z;\n",
+    "    vec3 pos2 = pos.xyz;\n",
+    "    pos = u_projection * pos;\n",
+    "    //gl_FragDepth = 0.5*(pos.z / pos.w)+0.5;\n",
+    "    vec3 normal = vec3(x,y,z);\n",
+    "    float diffuse = clamp(dot(normal, v_light_direction), 0.0, 1.0);\n",
+    "\n",
+    "    // Specular lighting.\n",
+    "    vec3 M = pos2.xyz;\n",
+    "    vec3 O = v_eye_position.xyz;\n",
+    "    vec3 L = u_light_spec_position;\n",
+    "    vec3 K = normalize(normalize(L - M) + normalize(O - M));\n",
+    "    // WARNING: abs() is necessary, otherwise weird bugs may appear with some\n",
+    "    // GPU drivers...\n",
+    "    float specular = clamp(pow(abs(dot(normal, K)), 40.), 0.0, 1.0);\n",
+    "    vec3 v_light = vec3(1., 1., 1.);\n",
+    "    gl_FragColor.rgb = (.15*v_color + .55*diffuse * v_color\n",
+    "                        + .35*specular * v_light);\n",
+    "}\n",
+    "\"\"\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "class Canvas(app.Canvas):\n",
+    "\n",
+    "    def __init__(self):\n",
+    "        app.Canvas.__init__(self, title='Molecular viewer',\n",
+    "                            keys='interactive', size=(640, 480))\n",
+    "        self.ps = self.pixel_scale\n",
+    "\n",
+    "        self.translate = 40\n",
+    "        self.program = gloo.Program(vertex, fragment)\n",
+    "        self.view = translate((0, 0, -self.translate))\n",
+    "        self.model = np.eye(4, dtype=np.float32)\n",
+    "        self.projection = np.eye(4, dtype=np.float32)\n",
+    "\n",
+    "        self.apply_zoom()\n",
+    "\n",
+    "        fname = load_data_file('molecular_viewer/micelle.npz')\n",
+    "        self.load_molecule(fname)\n",
+    "        self.load_data()\n",
+    "\n",
+    "        self.theta = 0\n",
+    "        self.phi = 0\n",
+    "\n",
+    "        gloo.set_state(depth_test=True, clear_color='black')\n",
+    "        self._timer = app.Timer('auto', connect=self.on_timer, start=True)\n",
+    "\n",
+    "        self.show()\n",
+    "\n",
+    "    def load_molecule(self, fname):\n",
+    "        molecule = np.load(fname)['molecule']\n",
+    "        self._nAtoms = molecule.shape[0]\n",
+    "\n",
+    "        # The x,y,z values store in one array\n",
+    "        self.coords = molecule[:, :3]\n",
+    "\n",
+    "        # The array that will store the color and alpha scale for all the atoms\n",
+    "        self.atomsColours = molecule[:, 3:6]\n",
+    "\n",
+    "        # The array that will store the scale for all the atoms.\n",
+    "        self.atomsScales = molecule[:, 6]\n",
+    "\n",
+    "    def load_data(self):\n",
+    "        n = self._nAtoms\n",
+    "\n",
+    "        data = np.zeros(n, [('a_position', np.float32, 3),\n",
+    "                            ('a_color', np.float32, 3),\n",
+    "                            ('a_radius', np.float32, 1)])\n",
+    "\n",
+    "        data['a_position'] = self.coords\n",
+    "        data['a_color'] = self.atomsColours\n",
+    "        data['a_radius'] = self.atomsScales*self.ps\n",
+    "\n",
+    "        self.program.bind(gloo.VertexBuffer(data))\n",
+    "\n",
+    "        self.program['u_model'] = self.model\n",
+    "        self.program['u_view'] = self.view\n",
+    "        self.program['u_light_position'] = 0., 0., 2.\n",
+    "        self.program['u_light_spec_position'] = -5., 5., -5.\n",
+    "\n",
+    "    def on_key_press(self, event):\n",
+    "        if event.text == ' ':\n",
+    "            if self.timer.running:\n",
+    "                self.timer.stop()\n",
+    "            else:\n",
+    "                self.timer.start()\n",
+    "        # if event.text == 'A':\n",
+    "            # self.\n",
+    "\n",
+    "    def on_timer(self, event):\n",
+    "        self.theta += .25\n",
+    "        self.phi += .25\n",
+    "        self.model = np.dot(rotate(self.theta, (0, 0, 1)),\n",
+    "                            rotate(self.phi, (0, 1, 0)))\n",
+    "        self.program['u_model'] = self.model\n",
+    "        self.update()\n",
+    "\n",
+    "    def on_resize(self, event):\n",
+    "        width, height = event.size\n",
+    "\n",
+    "    def apply_zoom(self):\n",
+    "        width, height = self.physical_size\n",
+    "        gloo.set_viewport(0, 0, width, height)\n",
+    "        self.projection = perspective(25.0, width / float(height), 2.0, 100.0)\n",
+    "        self.program['u_projection'] = self.projection\n",
+    "\n",
+    "    def on_mouse_wheel(self, event):\n",
+    "        self.translate -= event.delta[1]\n",
+    "        self.translate = max(-1, self.translate)\n",
+    "        self.view = translate((0, 0, -self.translate))\n",
+    "\n",
+    "        self.program['u_view'] = self.view\n",
+    "        self.update()\n",
+    "\n",
+    "    def on_draw(self, event):\n",
+    "        gloo.clear()\n",
+    "        self.program.draw('points')\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "c = Canvas()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "#c._timer.stop()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.4.3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/examples/tutorial/app/app_events.py b/examples/tutorial/app/app_events.py
index 1e0474a..8a00957 100644
--- a/examples/tutorial/app/app_events.py
+++ b/examples/tutorial/app/app_events.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 This example shows how to retrieve event information from a callback.
@@ -16,9 +16,6 @@ class Canvas(app.Canvas):
         app.Canvas.__init__(self, *args, **kwargs)
         self.title = 'App demo'
 
-    def on_initialize(self, event):
-        print('initializing!')
-
     def on_close(self, event):
         print('closing!')
 
@@ -42,8 +39,8 @@ class Canvas(app.Canvas):
         self.print_mouse_event(event, 'Mouse release')
 
     def on_mouse_move(self, event):
-        if (event.pos[0] < self.size[0] * 0.5
-                and event.pos[1] < self.size[1] * 0.5):
+        if (event.pos[0] < self.size[0] * 0.5 and
+                event.pos[1] < self.size[1] * 0.5):
             self.print_mouse_event(event, 'Mouse move')
 
     def on_mouse_wheel(self, event):
diff --git a/examples/tutorial/app/fps.py b/examples/tutorial/app/fps.py
index c7daf55..1fcf1cd 100644
--- a/examples/tutorial/app/fps.py
+++ b/examples/tutorial/app/fps.py
@@ -1,12 +1,12 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 This is a very minimal example that opens a window and makes the background
 color to change from black to white to black ...
 
-The backend (one of 'qt', 'glut', 'pyglet') is chosen automatically depending
-on what is available on your machine.
+The backend is chosen automatically depending on what is available on
+your machine.
 """
 
 import math
@@ -19,6 +19,7 @@ class Canvas(app.Canvas):
         app.Canvas.__init__(self, *args, **kwargs)
         self._timer = app.Timer('auto', connect=self.on_timer, start=True)
         self.tick = 0
+        self.show()
 
     def on_draw(self, event):
         gloo.clear(color=True)
@@ -34,6 +35,5 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     canvas = Canvas(keys='interactive')
-    canvas.show()
     canvas.measure_fps(1, canvas.show_fps)
     app.run()
diff --git a/examples/tutorial/app/interactive.py b/examples/tutorial/app/interactive.py
new file mode 100644
index 0000000..4742080
--- /dev/null
+++ b/examples/tutorial/app/interactive.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+"""
+This example shows how to configure VisPy to run from IPython (or Python) in
+interactive mode, while simultaneously updating the VisPy event loop.  This
+behavior is supported by default in all code that calls vispy.app.run(), but
+here it's setup manually.
+
+Run this file with `ipython -i interactive.py` to get a console and a window.
+"""
+
+import math
+from vispy import app, gloo
+from vispy.color import Color
+
+
+class Canvas(app.Canvas):
+
+    def __init__(self, *args, **kwargs):
+        app.Canvas.__init__(self, *args, **kwargs)
+        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
+        self.color = 'white'
+
+    def on_draw(self, event):
+        gloo.clear(color=True)
+
+    def on_timer(self, event):
+        # Animation speed based on global time.
+        t = event.elapsed
+        c = Color(self.color).rgb
+        # Simple sinusoid wave animation.
+        s = abs(0.5 + 0.5 * math.sin(t))
+        gloo.set_clear_color((c[0] * s, c[1] * s, c[2] * s, 1))
+        self.update()
+
+
+# You should run this demo as main with ipython -i <file>.  If interactive
+# mode is not specified, this demo will exit immediately because this demo
+# doesn't call run and relies on the input hook being setup.
+if __name__ == '__main__':
+    from vispy import app
+    # app.use_app('glfw')  # for testing specific backends
+    app.set_interactive()
+
+
+# All variables listed in this scope are accessible via the console.
+canvas = Canvas(keys='interactive')
+canvas.show()
+
+
+# In IPython, try typing any of the following:
+#   >>> canvas.color = (1.0, 0.0, 0.0)
+#   >>> canvas.color = 'red'
diff --git a/examples/tutorial/app/shared_context.py b/examples/tutorial/app/shared_context.py
index 8735519..f631da2 100644
--- a/examples/tutorial/app/shared_context.py
+++ b/examples/tutorial/app/shared_context.py
@@ -1,6 +1,9 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# vispy: testskip
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
 """
 This is a very simple example that demonstrates using a shared context
 between two Qt widgets.
@@ -37,7 +40,7 @@ class Window(QtGui.QWidget):
         box.addWidget(self.canvas_0.native)
 
         # pass the context from the first canvas to the second
-        self.canvas_1 = SceneCanvas(bgcolor='w', context=self.canvas_0.context)
+        self.canvas_1 = SceneCanvas(bgcolor='w', shared=self.canvas_0.context)
         self.vb_1 = ViewBox(parent=self.canvas_1.scene, bgcolor='b')
         self.vb_1.camera.rect = -1, -1, 2, 2
         self.canvas_1.events.resize.connect(partial(on_resize,
diff --git a/examples/tutorial/app/simple.py b/examples/tutorial/app/simple.py
index ad94d4a..d56c5ad 100644
--- a/examples/tutorial/app/simple.py
+++ b/examples/tutorial/app/simple.py
@@ -1,12 +1,12 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 This is a very minimal example that opens a window and makes the background
 color to change from black to white to black ...
 
-The backend (one of 'qt', 'glut', 'pyglet') is chosen automatically depending
-on what is available on your machine.
+The backend is chosen automatically depending on what is available on
+your machine.
 """
 
 import math
@@ -30,6 +30,6 @@ class Canvas(app.Canvas):
         self.update()
 
 if __name__ == '__main__':
-    canvas = Canvas(keys='interactive')
+    canvas = Canvas(keys='interactive', always_on_top=True)
     canvas.show()
     app.run()
diff --git a/examples/tutorial/app/simple_wx.py b/examples/tutorial/app/simple_wx.py
new file mode 100644
index 0000000..8a16d12
--- /dev/null
+++ b/examples/tutorial/app/simple_wx.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# vispy: testskip
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+"""
+This is a very minimal example that opens a window and makes the background
+color to change from black to white to black ...
+
+The wx backend is used to embed the canvas in a simple wx Frame with
+a menubar.
+"""
+
+import wx
+import math
+from vispy import app, gloo
+
+
+class Canvas(app.Canvas):
+    def __init__(self, *args, **kwargs):
+        app.Canvas.__init__(self, *args, **kwargs)
+        self._timer = app.Timer('auto', connect=self.on_timer, start=True)
+        self.tick = 0
+
+    def on_draw(self, event):
+        gloo.clear(color=True)
+
+    def on_timer(self, event):
+        self.tick += 1 / 60.0
+        c = abs(math.sin(self.tick))
+        gloo.set_clear_color((c, c, c, 1))
+        self.update()
+
+
+class TestFrame(wx.Frame):
+    def __init__(self):
+        wx.Frame.__init__(self, None, -1, "Vispy Test",
+                          wx.DefaultPosition, size=(500, 500))
+
+        MenuBar = wx.MenuBar()
+        file_menu = wx.Menu()
+        file_menu.Append(wx.ID_EXIT, "&Quit")
+        self.Bind(wx.EVT_MENU, self.on_quit, id=wx.ID_EXIT)
+        MenuBar.Append(file_menu, "&File")
+        self.SetMenuBar(MenuBar)
+
+        self.canvas = Canvas(app="wx", parent=self)
+        self.canvas.native.Show()
+
+    def on_quit(self, event):
+        self.Close(True)
+
+if __name__ == '__main__':
+    myapp = wx.App(0)
+    frame = TestFrame()
+    frame.Show(True)
+    myapp.MainLoop()
diff --git a/examples/tutorial/gl/cube.py b/examples/tutorial/gl/cube.py
index 7556829..5d57b4e 100644
--- a/examples/tutorial/gl/cube.py
+++ b/examples/tutorial/gl/cube.py
@@ -1,23 +1,22 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
 # Date:   04/03/2014
 # -----------------------------------------------------------------------------
 import math
-import ctypes
 import numpy as np
 
 from vispy import app
-from OpenGL import GL as gl
+from vispy.gloo import gl
 
 
 def checkerboard(grid_num=8, grid_size=32):
-    row_even = grid_num / 2 * [0, 1]
-    row_odd = grid_num / 2 * [1, 0]
-    Z = np.row_stack(grid_num / 2 * (row_even, row_odd)).astype(np.uint8)
+    row_even = grid_num // 2 * [0, 1]
+    row_odd = grid_num // 2 * [1, 0]
+    Z = np.row_stack(grid_num // 2 * (row_even, row_odd)).astype(np.uint8)
     return 255 * Z.repeat(grid_size, axis=0).repeat(grid_size, axis=1)
 
 
@@ -91,7 +90,7 @@ def makecube():
 
     indices = np.resize(
         np.array([0, 1, 2, 0, 2, 3], dtype=itype), 6 * (2 * 3))
-    indices += np.repeat(4 * np.arange(6), 6)
+    indices += np.repeat(4 * np.arange(6), 6).astype(np.uint32)
 
     return vertices, indices
 
@@ -125,7 +124,6 @@ class Canvas(app.Canvas):
         app.Canvas.__init__(self, size=(512, 512),
                             title='Rotating cube (GL version)',
                             keys='interactive')
-        self.timer = app.Timer('auto', self.on_timer)
 
     def on_initialize(self, event):
         # Build & activate cube program
@@ -145,31 +143,29 @@ class Canvas(app.Canvas):
 
         # Get data & build cube buffers
         vcube_data, self.icube_data = makecube()
-        vcube = gl.glGenBuffers(1)
+        vcube = gl.glCreateBuffer()
         gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vcube)
-        gl.glBufferData(gl.GL_ARRAY_BUFFER, vcube_data.nbytes,
-                        vcube_data, gl.GL_STATIC_DRAW)
-        icube = gl.glGenBuffers(1)
+        gl.glBufferData(gl.GL_ARRAY_BUFFER, vcube_data, gl.GL_STATIC_DRAW)
+        icube = gl.glCreateBuffer()
         gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, icube)
         gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER,
-                        self.icube_data.nbytes, self.icube_data,
-                        gl.GL_STATIC_DRAW)
+                        self.icube_data, gl.GL_STATIC_DRAW)
 
         # Bind cube attributes
         stride = vcube_data.strides[0]
-        offset = ctypes.c_void_p(0)
+        offset = 0
         loc = gl.glGetAttribLocation(self.cube, "a_position")
         gl.glEnableVertexAttribArray(loc)
         gl.glVertexAttribPointer(loc, 3, gl.GL_FLOAT, False, stride, offset)
 
-        offset = ctypes.c_void_p(vcube_data.dtype["a_position"].itemsize)
+        offset = vcube_data.dtype["a_position"].itemsize
         loc = gl.glGetAttribLocation(self.cube, "a_texcoord")
         gl.glEnableVertexAttribArray(loc)
         gl.glVertexAttribPointer(loc, 2, gl.GL_FLOAT, False, stride, offset)
 
         # Create & bind cube texture
         crate = checkerboard()
-        texture = gl.glGenTextures(1)
+        texture = gl.glCreateTexture()
         gl.glTexParameterf(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER,
                            gl.GL_LINEAR)
         gl.glTexParameterf(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER,
@@ -178,9 +174,10 @@ class Canvas(app.Canvas):
                            gl.GL_CLAMP_TO_EDGE)
         gl.glTexParameterf(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T,
                            gl.GL_CLAMP_TO_EDGE)
-        gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_INTENSITY,
-                        crate.shape[1], crate.shape[0],
-                        0, gl.GL_RED, gl.GL_UNSIGNED_BYTE, crate)
+        gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_LUMINANCE, gl.GL_LUMINANCE,
+                        gl.GL_UNSIGNED_BYTE, crate.shape[:2])
+        gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, 0, gl.GL_LUMINANCE,
+                           gl.GL_UNSIGNED_BYTE, crate)
         loc = gl.glGetUniformLocation(self.cube, "u_texture")
         gl.glUniform1i(loc, texture)
         gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
@@ -203,8 +200,8 @@ class Canvas(app.Canvas):
         # OpenGL initalization
         gl.glClearColor(0.30, 0.30, 0.35, 1.00)
         gl.glEnable(gl.GL_DEPTH_TEST)
-
-        self.timer.start()
+        self._resize(*(self.size + self.physical_size))
+        self.timer = app.Timer('auto', self.on_timer, start=True)
 
     def on_draw(self, event):
         gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
@@ -212,8 +209,10 @@ class Canvas(app.Canvas):
                           gl.GL_UNSIGNED_INT, None)
 
     def on_resize(self, event):
-        width, height = event.size
-        gl.glViewport(0, 0, width, height)
+        self._resize(*(event.size + event.physical_size))
+
+    def _resize(self, width, height, physical_width, physical_height):
+        gl.glViewport(0, 0, physical_width, physical_height)
         projection = perspective(35.0, width / float(height), 2.0, 10.0)
         loc = gl.glGetUniformLocation(self.cube, "u_projection")
         gl.glUniformMatrix4fv(loc, 1, False, projection)
diff --git a/examples/tutorial/gl/fireworks.py b/examples/tutorial/gl/fireworks.py
index 1c61150..a696b1a 100644
--- a/examples/tutorial/gl/fireworks.py
+++ b/examples/tutorial/gl/fireworks.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author:   Almar Klein & Nicolas P .Rougier
@@ -17,11 +17,10 @@ visualization during the explosion is highly optimized using a Vertex Buffer
 Object (VBO). After each explosion, vertex data for the next explosion are
 calculated, such that each explostion is unique.
 """
-import ctypes
 import numpy as np
-import OpenGL.GL as gl
 
 from vispy import app
+from vispy.gloo import gl
 
 
 vertex_code = """
@@ -65,7 +64,6 @@ class Canvas(app.Canvas):
     def __init__(self):
         app.Canvas.__init__(self, size=(800, 600), title='GL Fireworks',
                             keys='interactive')
-        self.timer = app.Timer('auto', self.on_timer)
 
     def on_initialize(self, event):
         # Build & activate program
@@ -88,25 +86,24 @@ class Canvas(app.Canvas):
         self.data = np.zeros(n, dtype=[('lifetime', np.float32, 1),
                                        ('start',    np.float32, 3),
                                        ('end',      np.float32, 3)])
-        vbuffer = gl.glGenBuffers(1)
+        vbuffer = gl.glCreateBuffer()
         gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbuffer)
-        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.data.nbytes, self.data,
-                        gl.GL_DYNAMIC_DRAW)
+        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.data, gl.GL_DYNAMIC_DRAW)
 
         # Bind buffer attributes
         stride = self.data.strides[0]
 
-        offset = ctypes.c_void_p(0)
+        offset = 0
         loc = gl.glGetAttribLocation(self.program, "lifetime")
         gl.glEnableVertexAttribArray(loc)
         gl.glVertexAttribPointer(loc, 1, gl.GL_FLOAT, False, stride, offset)
 
-        offset = ctypes.c_void_p(self.data.dtype["lifetime"].itemsize)
+        offset = self.data.dtype["lifetime"].itemsize
         loc = gl.glGetAttribLocation(self.program, "start")
         gl.glEnableVertexAttribArray(loc)
         gl.glVertexAttribPointer(loc, 3, gl.GL_FLOAT, False, stride, offset)
 
-        offset = ctypes.c_void_p(self.data.dtype["start"].itemsize)
+        offset = self.data.dtype["start"].itemsize
         loc = gl.glGetAttribLocation(self.program, "end")
         gl.glEnableVertexAttribArray(loc)
         gl.glVertexAttribPointer(loc, 3, gl.GL_FLOAT, False, stride, offset)
@@ -117,17 +114,18 @@ class Canvas(app.Canvas):
         gl.glDisable(gl.GL_DEPTH_TEST)
         gl.glEnable(gl.GL_BLEND)
         gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE)
-        gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE)
-        gl.glEnable(gl.GL_POINT_SPRITE)
+        gl.glEnable(34370)  # gl.GL_VERTEX_PROGRAM_POINT_SIZE
+        gl.glEnable(34913)  # gl.GL_POINT_SPRITE
+        gl.glViewport(0, 0, *self.physical_size)
         self.new_explosion()
-        self.timer.start()
+        self.timer = app.Timer('auto', self.on_timer, start=True)
 
     def on_draw(self, event):
         gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
         gl.glDrawArrays(gl.GL_POINTS, 0, len(self.data))
 
     def on_resize(self, event):
-        gl.glViewport(0, 0, *event.size)
+        gl.glViewport(0, 0, *event.physical_size)
 
     def on_timer(self, event):
         self.elapsed_time += 1. / 60.
@@ -153,8 +151,7 @@ class Canvas(app.Canvas):
         self.data['lifetime'] = np.random.normal(2.0, 0.5, (n,))
         self.data['start'] = np.random.normal(0.0, 0.2, (n, 3))
         self.data['end'] = np.random.normal(0.0, 1.2, (n, 3))
-        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.data.nbytes, self.data,
-                        gl.GL_DYNAMIC_DRAW)
+        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.data, gl.GL_DYNAMIC_DRAW)
 
 if __name__ == '__main__':
     c = Canvas()
diff --git a/examples/tutorial/gl/quad.py b/examples/tutorial/gl/quad.py
index eee3fb6..d9d1cd8 100644
--- a/examples/tutorial/gl/quad.py
+++ b/examples/tutorial/gl/quad.py
@@ -1,16 +1,15 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
 # Date:   04/03/2014
 # -----------------------------------------------------------------------------
-import ctypes
 import numpy as np
-import OpenGL.GL as gl
 
 from vispy import app
+from vispy.gloo import gl
 
 vertex_code = """
     uniform float scale;
@@ -77,24 +76,23 @@ class Canvas(app.Canvas):
         # Build buffer
 
         # Request a buffer slot from GPU
-        buf = gl.glGenBuffers(1)
+        buf = gl.glCreateBuffer()
 
         # Make this buffer the default one
         gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buf)
 
         # Upload data
-        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.data.nbytes, self.data,
-                        gl.GL_DYNAMIC_DRAW)
+        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.data, gl.GL_DYNAMIC_DRAW)
 
         # Bind attributes
         stride = self.data.strides[0]
-        offset = ctypes.c_void_p(0)
+        offset = 0
         loc = gl.glGetAttribLocation(program, "position")
         gl.glEnableVertexAttribArray(loc)
         gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buf)
         gl.glVertexAttribPointer(loc, 3, gl.GL_FLOAT, False, stride, offset)
 
-        offset = ctypes.c_void_p(self.data.dtype["position"].itemsize)
+        offset = self.data.dtype["position"].itemsize
         loc = gl.glGetAttribLocation(program, "color")
         gl.glEnableVertexAttribArray(loc)
         gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buf)
@@ -110,7 +108,7 @@ class Canvas(app.Canvas):
         gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
 
     def on_resize(self, event):
-        gl.glViewport(0, 0, *event.size)
+        gl.glViewport(0, 0, *event.physical_size)
 
 if __name__ == '__main__':
     c = Canvas()
diff --git a/examples/tutorial/gloo/colored_cube.py b/examples/tutorial/gloo/colored_cube.py
index 7ebe8a3..9e08b08 100644
--- a/examples/tutorial/gloo/colored_cube.py
+++ b/examples/tutorial/gloo/colored_cube.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
@@ -19,8 +19,12 @@ vertex = """
 uniform mat4 model;
 uniform mat4 view;
 uniform mat4 projection;
+
 attribute vec3 position;
+attribute vec2 texcoord;
+attribute vec3 normal;
 attribute vec4 color;
+
 varying vec4 v_color;
 void main()
 {
@@ -42,9 +46,7 @@ class Canvas(app.Canvas):
     def __init__(self):
         app.Canvas.__init__(self, size=(512, 512), title='Colored cube',
                             keys='interactive')
-        self.timer = app.Timer('auto', self.on_timer)
 
-    def on_initialize(self, event):
         # Build cube data
         V, I, _ = create_cube()
         vertices = VertexBuffer(V)
@@ -55,35 +57,39 @@ class Canvas(app.Canvas):
         self.program.bind(vertices)
 
         # Build view, model, projection & normal
-        view = np.eye(4, dtype=np.float32)
+        view = translate((0, 0, -5))
         model = np.eye(4, dtype=np.float32)
-        translate(view, 0, 0, -5)
         self.program['model'] = model
         self.program['view'] = view
         self.phi, self.theta = 0, 0
         gloo.set_state(clear_color=(0.30, 0.30, 0.35, 1.00), depth_test=True)
-        self.timer.start()
+
+        self.activate_zoom()
+
+        self.timer = app.Timer('auto', self.on_timer, start=True)
+
+        self.show()
 
     def on_draw(self, event):
         gloo.clear(color=True, depth=True)
         self.program.draw('triangles', self.indices)
 
     def on_resize(self, event):
-        gloo.set_viewport(0, 0, *event.size)
-        projection = perspective(45.0, event.size[0] / float(event.size[1]),
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        gloo.set_viewport(0, 0, *self.physical_size)
+        projection = perspective(45.0, self.size[0] / float(self.size[1]),
                                  2.0, 10.0)
         self.program['projection'] = projection
 
     def on_timer(self, event):
         self.theta += .5
         self.phi += .5
-        model = np.eye(4, dtype=np.float32)
-        rotate(model, self.theta, 0, 0, 1)
-        rotate(model, self.phi, 0, 1, 0)
-        self.program['model'] = model
+        self.program['model'] = np.dot(rotate(self.theta, (0, 0, 1)),
+                                       rotate(self.phi, (0, 1, 0)))
         self.update()
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/tutorial/gloo/colored_quad.py b/examples/tutorial/gloo/colored_quad.py
index 5abbeca..13b1d50 100644
--- a/examples/tutorial/gloo/colored_quad.py
+++ b/examples/tutorial/gloo/colored_quad.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
@@ -33,7 +33,6 @@ class Canvas(app.Canvas):
         app.Canvas.__init__(self, size=(512, 512), title='Colored quad',
                             keys='interactive')
 
-    def on_initialize(self, event):
         # Build program & data
         self.program = Program(vertex, fragment, count=4)
         self.program['color'] = [(1, 0, 0, 1), (0, 1, 0, 1),
@@ -41,14 +40,17 @@ class Canvas(app.Canvas):
         self.program['position'] = [(-1, -1), (-1, +1),
                                     (+1, -1), (+1, +1)]
 
+        gloo.set_viewport(0, 0, *self.physical_size)
+
+        self.show()
+
     def on_draw(self, event):
         gloo.clear(color='white')
         self.program.draw('triangle_strip')
 
     def on_resize(self, event):
-        gloo.set_viewport(0, 0, *event.size)
+        gloo.set_viewport(0, 0, *event.physical_size)
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/tutorial/gloo/lighted_cube.py b/examples/tutorial/gloo/lighted_cube.py
index 2967f06..fbec790 100644
--- a/examples/tutorial/gloo/lighted_cube.py
+++ b/examples/tutorial/gloo/lighted_cube.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
@@ -22,6 +22,7 @@ uniform mat4 u_projection;
 uniform vec4 u_color;
 
 attribute vec3 position;
+attribute vec2 texcoord;
 attribute vec3 normal;
 attribute vec4 color;
 
@@ -82,7 +83,6 @@ class Canvas(app.Canvas):
                             keys='interactive')
         self.timer = app.Timer('auto', self.on_timer)
 
-    def on_initialize(self, event):
         # Build cube data
         V, F, O = create_cube()
         vertices = VertexBuffer(V)
@@ -91,9 +91,8 @@ class Canvas(app.Canvas):
 
         # Build view, model, projection & normal
         # --------------------------------------
-        self.view = np.eye(4, dtype=np.float32)
+        self.view = translate((0, 0, -5))
         model = np.eye(4, dtype=np.float32)
-        translate(self.view, 0, 0, -5)
         normal = np.array(np.matrix(np.dot(self.view, model)).I.T)
 
         # Build program
@@ -107,7 +106,9 @@ class Canvas(app.Canvas):
         self.program["u_normal"] = normal
         self.phi, self.theta = 0, 0
 
-        # OpenGL initalization
+        self.activate_zoom()
+
+        # OpenGL initialization
         # --------------------------------------
         gloo.set_state(clear_color=(0.30, 0.30, 0.35, 1.00), depth_test=True,
                        polygon_offset=(1, 1),
@@ -115,6 +116,8 @@ class Canvas(app.Canvas):
                        line_width=0.75)
         self.timer.start()
 
+        self.show()
+
     def on_draw(self, event):
         gloo.clear(color=True, depth=True)
         # program.draw(gl.GL_TRIANGLES, indices)
@@ -131,23 +134,24 @@ class Canvas(app.Canvas):
         gloo.set_state(depth_mask=True)
 
     def on_resize(self, event):
-        gloo.set_viewport(0, 0, *event.size)
-        projection = perspective(45.0, event.size[0] / float(event.size[1]),
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        gloo.set_viewport(0, 0, *self.physical_size)
+        projection = perspective(45.0, self.size[0] / float(self.size[1]),
                                  2.0, 10.0)
         self.program['u_projection'] = projection
 
     def on_timer(self, event):
         self.theta += .5
         self.phi += .5
-        model = np.eye(4, dtype=np.float32)
-        rotate(model, self.theta, 0, 0, 1)
-        rotate(model, self.phi, 0, 1, 0)
-        normal = np.array(np.matrix(np.dot(self.view, model)).I.T)
+        model = np.dot(rotate(self.theta, (0, 0, 1)),
+                       rotate(self.phi, (0, 1, 0)))
+        normal = np.linalg.inv(np.dot(self.view, model)).T
         self.program['u_model'] = model
         self.program['u_normal'] = normal
         self.update()
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/tutorial/gloo/outlined_cube.py b/examples/tutorial/gloo/outlined_cube.py
index 97b2903..941b179 100644
--- a/examples/tutorial/gloo/outlined_cube.py
+++ b/examples/tutorial/gloo/outlined_cube.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
@@ -19,8 +19,12 @@ uniform mat4 u_model;
 uniform mat4 u_view;
 uniform mat4 u_projection;
 uniform vec4 u_color;
+
 attribute vec3 position;
+attribute vec2 texcoord;
+attribute vec3 normal;
 attribute vec4 color;
+
 varying vec4 v_color;
 void main()
 {
@@ -44,7 +48,6 @@ class Canvas(app.Canvas):
                             keys='interactive')
         self.timer = app.Timer('auto', self.on_timer)
 
-    def on_initialize(self, event):
         # Build cube data
         V, I, O = create_cube()
         vertices = VertexBuffer(V)
@@ -58,20 +61,24 @@ class Canvas(app.Canvas):
 
         # Build view, model, projection & normal
         # --------------------------------------
-        view = np.eye(4, dtype=np.float32)
+        view = translate((0, 0, -5))
         model = np.eye(4, dtype=np.float32)
-        translate(view, 0, 0, -5)
+
         self.program['u_model'] = model
         self.program['u_view'] = view
         self.phi, self.theta = 0, 0
 
-        # OpenGL initalization
+        self.activate_zoom()
+
+        # OpenGL initialization
         # --------------------------------------
         gloo.set_state(clear_color=(0.30, 0.30, 0.35, 1.00), depth_test=True,
                        polygon_offset=(1, 1), line_width=0.75,
                        blend_func=('src_alpha', 'one_minus_src_alpha'))
         self.timer.start()
 
+        self.show()
+
     def on_draw(self, event):
         gloo.clear(color=True, depth=True)
 
@@ -87,21 +94,21 @@ class Canvas(app.Canvas):
         gloo.set_state(depth_mask=True)
 
     def on_resize(self, event):
-        gloo.set_viewport(0, 0, *event.size)
-        projection = perspective(45.0, event.size[0] / float(event.size[1]),
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        gloo.set_viewport(0, 0, *self.physical_size)
+        projection = perspective(45.0, self.size[0] / float(self.size[1]),
                                  2.0, 10.0)
         self.program['u_projection'] = projection
 
     def on_timer(self, event):
         self.theta += .5
         self.phi += .5
-        model = np.eye(4, dtype=np.float32)
-        rotate(model, self.theta, 0, 0, 1)
-        rotate(model, self.phi, 0, 1, 0)
-        self.program['u_model'] = model
+        self.program['u_model'] = np.dot(rotate(self.theta, (0, 0, 1)),
+                                         rotate(self.phi, (0, 1, 0)))
         self.update()
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/tutorial/gloo/rotating_quad.py b/examples/tutorial/gloo/rotating_quad.py
index 68a771b..ff60ea4 100644
--- a/examples/tutorial/gloo/rotating_quad.py
+++ b/examples/tutorial/gloo/rotating_quad.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
@@ -39,23 +39,28 @@ class Canvas(app.Canvas):
                             keys='interactive')
         self.timer = app.Timer('auto', self.on_timer)
 
-    def on_initialize(self, event):
         # Build program & data
         self.program = Program(vertex, fragment, count=4)
         self.program['color'] = [(1, 0, 0, 1), (0, 1, 0, 1),
                                  (0, 0, 1, 1), (1, 1, 0, 1)]
         self.program['position'] = [(-1, -1), (-1, +1),
                                     (+1, -1), (+1, +1)]
+        self.program['theta'] = 0.0
+
+        gloo.set_viewport(0, 0, *self.physical_size)
+
         self.clock = 0
         self.timer.start()
 
+        self.show()
+
     def on_draw(self, event):
         gloo.set_clear_color('white')
         gloo.clear(color=True)
         self.program.draw('triangle_strip')
 
     def on_resize(self, event):
-        gloo.set_viewport(0, 0, *event.size)
+        gloo.set_viewport(0, 0, *event.physical_size)
 
     def on_timer(self, event):
         self.clock += 0.001 * 1000.0 / 60.
@@ -64,5 +69,4 @@ class Canvas(app.Canvas):
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/tutorial/gloo/texture_precision.py b/examples/tutorial/gloo/texture_precision.py
new file mode 100644
index 0000000..3de1b5d
--- /dev/null
+++ b/examples/tutorial/gloo/texture_precision.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+
+# -----------------------------------------------------------------------------
+# Copyright 2015 University of Southern California.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+# Author: Karl Czajkowski <karlcz at isi.edu>
+# Date:   2015-01-22
+# -----------------------------------------------------------------------------
+
+"""Example using texture internalformat for higher precision.
+
+Generates a gradient texture with high dynamic range and renders it
+with a fragment shader that tests for quantization errors by comparing
+adjacent texels and decomposes the gradient values into high and low
+significance bits, mapping them to separate display color channels.
+
+Pressing the 'f' key cycles through a list of different texture
+formats to show the different levels of precision available.
+"""
+
+import numpy as np
+from vispy import gloo
+from vispy import app
+
+W, H = 1024, 1024
+
+# prepare a gradient field with high dynamic range
+data = np.zeros((H, W, 3), np.float32)
+
+for i in range(W):
+    data[:, i, :] = i**2
+
+for i in range(H):
+    data[i, :, :] *= i**2
+
+data *= 1./data.max()
+
+# prepare a simple quad to cover the viewport
+quad = np.zeros(4, dtype=[
+    ('a_position', np.float32, 2),
+    ('a_texcoord', np.float32, 2)
+])
+
+quad['a_position'] = np.array([[-1, -1], [+1, -1], [-1, +1], [+1, +1]])
+quad['a_texcoord'] = np.array([[0, 0], [1, 0], [0, 1], [1, 1]])
+
+vert_shader = """
+attribute vec2 a_position;
+attribute vec2 a_texcoord;
+varying vec2 v_texcoord;
+
+void main()
+{
+   v_texcoord = a_texcoord;
+   gl_Position = vec4(a_position, 0.0, 1.0);
+}
+"""
+
+frag_shader = """
+uniform sampler2D u_texture;
+varying vec2 v_texcoord;
+
+void main()
+{
+   float ndiff;
+   // an adjacent texel is 1/W further over in normalized texture coordinates
+   vec2 v_texcoord2 = vec2(clamp(v_texcoord.x + 1.0/%(W)d, 0.0, 1.0),
+                           v_texcoord.y);
+   vec4 texel1 = texture2D(u_texture, v_texcoord);
+   vec4 texel2 = texture2D(u_texture, v_texcoord2);
+
+   // test for quantized binning of adjacent texels
+   if (texel1.r == texel2.r && v_texcoord2.x < 1.0 && v_texcoord.y > 0.0)
+      ndiff = 1.0;
+   else
+      ndiff = 0.0;
+
+   gl_FragColor = vec4(
+      fract(texel1.r * 255.0),  // render low-significance bits as red
+      texel1.r,                 // render high-significance bits as green
+      ndiff,                    // flag quantized bands as blue
+      1);
+}
+""" % dict(W=W)
+
+
+class Canvas(app.Canvas):
+
+    def __init__(self):
+        app.Canvas.__init__(self, size=(W, H), keys='interactive')
+
+        self._internalformats = [
+            'rgb8',
+            'rgb16',
+            'rgb16f',
+            'rgb32f'
+        ]
+
+        self.program = gloo.Program(vert_shader, frag_shader)
+        self.program.bind(gloo.VertexBuffer(quad))
+        self._internalformat = -1
+        self.texture = gloo.Texture2D(
+            shape=(H, W, 3),
+            interpolation='nearest'
+        )
+
+        gloo.set_viewport(0, 0, *self.physical_size)
+
+        self.toggle_internalformat()
+
+        self.show()
+
+    def on_key_press(self, event):
+        if event.key == 'F':
+            self.toggle_internalformat()
+
+    def toggle_internalformat(self):
+        self._internalformat = (
+            (self._internalformat + 1)
+            % len(self._internalformats)
+        )
+        internalformat = self._internalformats[self._internalformat]
+        print("Requesting texture internalformat %s" % internalformat)
+        self.texture.resize(
+            data.shape,
+            format='rgb',
+            internalformat=internalformat
+        )
+        self.texture.set_data(data)
+        self.program['u_texture'] = self.texture
+        self.update()
+
+    def on_resize(self, event):
+        gloo.set_viewport(0, 0, *event.physical_size)
+
+    def on_draw(self, event):
+        gloo.clear(color=True, depth=True)
+        self.program.draw('triangle_strip')
+
+if __name__ == '__main__':
+    c = Canvas()
+    app.run()
diff --git a/examples/tutorial/gloo/textured_cube.py b/examples/tutorial/gloo/textured_cube.py
index b5c74f8..edd3a64 100644
--- a/examples/tutorial/gloo/textured_cube.py
+++ b/examples/tutorial/gloo/textured_cube.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
@@ -22,6 +22,8 @@ uniform sampler2D texture;
 
 attribute vec3 position;
 attribute vec2 texcoord;
+attribute vec3 normal;
+attribute vec4 color;
 
 varying vec2 v_texcoord;
 void main()
@@ -42,9 +44,9 @@ void main()
 
 
 def checkerboard(grid_num=8, grid_size=32):
-    row_even = grid_num / 2 * [0, 1]
-    row_odd = grid_num / 2 * [1, 0]
-    Z = np.row_stack(grid_num / 2 * (row_even, row_odd)).astype(np.uint8)
+    row_even = grid_num // 2 * [0, 1]
+    row_odd = grid_num // 2 * [1, 0]
+    Z = np.row_stack(grid_num // 2 * (row_even, row_odd)).astype(np.uint8)
     return 255 * Z.repeat(grid_size, axis=0).repeat(grid_size, axis=1)
 
 
@@ -54,7 +56,6 @@ class Canvas(app.Canvas):
                             keys='interactive')
         self.timer = app.Timer('auto', self.on_timer)
 
-    def on_initialize(self, event):
         # Build cube data
         V, I, _ = create_cube()
         vertices = VertexBuffer(V)
@@ -65,39 +66,42 @@ class Canvas(app.Canvas):
         self.program.bind(vertices)
 
         # Build view, model, projection & normal
-        view = np.eye(4, dtype=np.float32)
+        view = translate((0, 0, -5))
         model = np.eye(4, dtype=np.float32)
-        translate(view, 0, 0, -5)
         self.program['model'] = model
         self.program['view'] = view
         self.program['texture'] = checkerboard()
 
+        self.activate_zoom()
+
         self.phi, self.theta = 0, 0
 
         # OpenGL initalization
         gloo.set_state(clear_color=(0.30, 0.30, 0.35, 1.00), depth_test=True)
         self.timer.start()
 
+        self.show()
+
     def on_draw(self, event):
         gloo.clear(color=True, depth=True)
         self.program.draw('triangles', self.indices)
 
     def on_resize(self, event):
-        gloo.set_viewport(0, 0, *event.size)
-        projection = perspective(45.0, event.size[0] / float(event.size[1]),
+        self.activate_zoom()
+
+    def activate_zoom(self):
+        gloo.set_viewport(0, 0, *self.physical_size)
+        projection = perspective(45.0, self.size[0] / float(self.size[1]),
                                  2.0, 10.0)
         self.program['projection'] = projection
 
     def on_timer(self, event):
         self.theta += .5
         self.phi += .5
-        model = np.eye(4, dtype=np.float32)
-        rotate(model, self.theta, 0, 0, 1)
-        rotate(model, self.phi, 0, 1, 0)
-        self.program['model'] = model
+        self.program['model'] = np.dot(rotate(self.theta, (0, 0, 1)),
+                                       rotate(self.phi, (0, 1, 0)))
         self.update()
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/tutorial/gloo/textured_quad.py b/examples/tutorial/gloo/textured_quad.py
index d60a908..42432ac 100644
--- a/examples/tutorial/gloo/textured_quad.py
+++ b/examples/tutorial/gloo/textured_quad.py
@@ -29,9 +29,9 @@ fragment = """
 
 
 def checkerboard(grid_num=8, grid_size=32):
-    row_even = grid_num / 2 * [0, 1]
-    row_odd = grid_num / 2 * [1, 0]
-    Z = np.row_stack(grid_num / 2 * (row_even, row_odd)).astype(np.uint8)
+    row_even = grid_num // 2 * [0, 1]
+    row_odd = grid_num // 2 * [1, 0]
+    Z = np.row_stack(grid_num // 2 * (row_even, row_odd)).astype(np.uint8)
     return 255 * Z.repeat(grid_size, axis=0).repeat(grid_size, axis=1)
 
 
@@ -40,7 +40,6 @@ class Canvas(app.Canvas):
         app.Canvas.__init__(self, size=(512, 512), title='Textured quad',
                             keys='interactive')
 
-    def on_initialize(self, event):
         # Build program & data
         self.program = Program(vertex, fragment, count=4)
         self.program['position'] = [(-1, -1), (-1, +1),
@@ -48,15 +47,18 @@ class Canvas(app.Canvas):
         self.program['texcoord'] = [(0, 0), (1, 0), (0, 1), (1, 1)]
         self.program['texture'] = checkerboard()
 
+        gloo.set_viewport(0, 0, *self.physical_size)
+
+        self.show()
+
     def on_draw(self, event):
         gloo.set_clear_color('white')
         gloo.clear(color=True)
         self.program.draw('triangle_strip')
 
     def on_resize(self, event):
-        gloo.set_viewport(0, 0, *event.size)
+        gloo.set_viewport(0, 0, *event.physical_size)
 
 if __name__ == '__main__':
     c = Canvas()
-    c.show()
     app.run()
diff --git a/examples/tutorial/visuals/T01_basic_visual.py b/examples/tutorial/visuals/T01_basic_visual.py
new file mode 100644
index 0000000..1401237
--- /dev/null
+++ b/examples/tutorial/visuals/T01_basic_visual.py
@@ -0,0 +1,167 @@
+"""
+Tutorial: Creating Visuals
+--------------------------
+
+This tutorial is intended to guide developers who are interested in creating 
+new subclasses of Visual. In most cases, this will not be necessary because
+vispy's base library of visuals will be sufficient to create complex scenes as
+needed. However, there are cases where a particular visual effect is desired 
+that is not supported in the base library, or when a custom visual is needed to
+optimize performance for a specific use case.
+
+The purpose of a Visual is to encapsulate a single drawable object. This
+drawable can be as simple or complex as desired. Some of the simplest visuals 
+draw points, lines, or triangles, whereas more complex visuals invove multiple
+drawing stages or make use of sub-visuals to construct larger objects.
+
+In this example we will create a very simple Visual that draws a rectangle.
+Visuals are defined by:
+
+1. Creating a subclass of vispy.visuals.Visual
+2. Defining a draw() method that takes into account some user-specified
+   transformation functions.
+
+
+"""
+from vispy import app, gloo, visuals, scene
+import numpy as np
+
+# Define a simple vertex shader. We use $template variables as placeholders for
+# code that will be inserted later on. In this example, $position will become
+# an attribute, and $transform will become a function. Important: using
+# $transform in this way ensures that users of this visual will be able to 
+# apply arbitrary transformations to it.
+vertex_shader = """
+void main() {
+   gl_Position = $transform(vec4($position, 0, 1));
+}
+"""
+
+# Very simple fragment shader. Again we use a template variable "$color", which
+# allows us to decide later how the color should be defined (in this case, we
+# will just use a uniform red color).
+fragment_shader = """
+void main() {
+  gl_FragColor = $color;
+}
+"""
+
+
+# Start the new Visual class. 
+# By convention, all Visual subclass names end in 'Visual'.
+# (Custom visuals may ignore this convention, but for visuals that are built 
+# in to vispy, this is required to ensure that the VisualNode subclasses are 
+# generated correctly.)
+class MyRectVisual(visuals.Visual):
+    """Visual that draws a red rectangle.
+    
+    Parameters
+    ----------
+    x : float
+        x coordinate of rectangle origin
+    y : float
+        y coordinate of rectangle origin
+    w : float
+        width of rectangle
+    h : float
+        height of rectangle
+        
+    All parameters are specified in the local (arbitrary) coordinate system of
+    the visual. How this coordinate system translates to the canvas will 
+    depend on the transformation functions used during drawing.
+    """
+    
+    # There are no constraints on the signature of the __init__ method; use
+    # whatever makes the most sense for your visual.
+    def __init__(self, x, y, w, h):
+        visuals.Visual.__init__(self)
+        
+        # vertices for two triangles forming a rectangle
+        self.vbo = gloo.VertexBuffer(np.array([
+            [x, y], [x+w, y], [x+w, y+h],
+            [x, y], [x+w, y+h], [x, y+h]
+        ], dtype=np.float32))
+        
+        # We use a ModularProgram because it allows us to plug in arbitrary 
+        # transformation functions. This is recommended but not strictly 
+        # required.
+        self.program = visuals.shaders.ModularProgram(vertex_shader, 
+                                                      fragment_shader)
+        
+        # Assign values to the $position and $color template variables in 
+        # the shaders. ModularProgram automatically handles generating the 
+        # necessary attribute and uniform declarations with unique variable
+        # names.
+        self.program.vert['position'] = self.vbo
+        self.program.frag['color'] = (1, 0, 0, 1)
+        
+    # The draw method is required to take a single argument that will be an
+    # instance of visuals.transforms.TransformSystem.
+    def draw(self, transforms):
+        # The TransformSystem provides information about:
+        # 
+        # * The transformation requested by the user (usually translation, 
+        #   rotation, and scaling)
+        # * The canvas dpi and the size of logical pixels
+        # * The relationship between physical pixels and logical pixels, which
+        #   is important for some high-resolution displays and when exporting 
+        #   to images
+        
+        # For the simple case of this visual, all we need to know is how to 
+        # convert from the user-specified coordinates (x, y, w, h) to the 
+        # normalized device coordinates required by the vertex shader. We will
+        # explore other uses of the TransformSystem in later tutorials.
+        self.program.vert['transform'] = transforms.get_full_transform()
+        
+        # Finally, draw the triangles.
+        self.program.draw('triangles')
+
+
+# At this point the visual is ready to use, but it takes some extra effort to
+# set up a Canvas and TransformSystem for drawing (the examples in 
+# examples/basics/visuals/ all follow this approach). 
+# 
+# An easier approach is to make the visual usable in a scenegraph, in which 
+# case the canvas will take care of drawing the visual and setting up the 
+# TransformSystem for us.
+# 
+# To be able to use our new Visual in a scenegraph, it needs to be
+# a subclass of scene.Node. In vispy we achieve this by creating a parallel
+# set of classes that inherit from both Node and each Visual subclass.
+# This can be done automatically using scene.visuals.create_visual_node():
+MyRect = scene.visuals.create_visual_node(MyRectVisual)
+
+# By convention, these classes have the same name as the Visual they inherit 
+# from, but without the 'Visual' suffix.
+
+# The auto-generated class MyRect is basically equivalent to::
+# 
+#     class MyRect(MyRectVisual, scene.Node):
+#        def __init__(self, *args, **kwds):
+#            parent = kwds.pop('parent', None)
+#            name = kwds.pop('name', None)
+#            MyRectVisual.__init__(self, *args, **kwds)
+#            Node.__init__(self, parent=parent, name=name)
+#         
+
+
+# Finally we will test the visual by displaying in a scene.
+
+# Create a canvas to display our visual
+canvas = scene.SceneCanvas(keys='interactive', show=True)
+
+# Create two instances of MyRect, each using canvas.scene as their parent
+rects = [MyRect(100, 100, 200, 300, parent=canvas.scene),
+         MyRect(500, 100, 200, 300, parent=canvas.scene)]
+
+# To test that the user-specified transforms work correctly, I'll rotate
+# one rectangle slightly.
+tr = visuals.transforms.AffineTransform()
+tr.rotate(5, (0, 0, 1))
+rects[1].transform = tr
+
+# ..and optionally start the event loop
+if __name__ == '__main__':
+    import sys
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/examples/tutorial/visuals/T02_measurements.py b/examples/tutorial/visuals/T02_measurements.py
new file mode 100644
index 0000000..5ab3a34
--- /dev/null
+++ b/examples/tutorial/visuals/T02_measurements.py
@@ -0,0 +1,227 @@
+"""
+Tutorial: Creating Visuals
+==========================
+
+02. Making physical measurements
+--------------------------------
+
+In the last tutorial we created a simple Visual subclass that draws a 
+rectangle. In this tutorial, we will make two additions:
+
+    1. Draw a rectangular border instead of a solid rectangle
+    2. Make the border a fixed pixel width, even when displayed inside a 
+       user-zoomable ViewBox. 
+
+The border is made by drawing a line_strip with 10 vertices::
+
+    1--------------3
+    |              |
+    |   2------4   |     [ note that points 9 and 10 are
+    |   |      |   |       the same as points 1 and 2 ]
+    |   8------6   |
+    |              |
+    7--------------5
+
+In order to ensure that the border has a fixed width in pixels, we need to 
+adjust the spacing between the inner and outer rectangles whenever the user
+changes the zoom of the ViewBox.
+
+How? Recall that each 
+time the visual is drawn, it is given a TransformSystem instance that carries
+information about the size of logical and physical pixels relative to the 
+visual [link to TransformSystem documentation]. Essentially, we have 4 
+coordinate systems:
+
+    Visual -> Document -> Framebuffer -> Render
+    
+The user specifies the position and size of the rectangle in Visual 
+coordinates, and in [tutorial 1] we used the vertex shader to convert directly
+from Visual coordinates to render coordinates. In this tutorial we will
+convert first to document coordinates, then make the adjustment for the border
+width, then convert the remainder of the way to render coordinates.
+
+Let's say, for example that the user specifies the box width to be 20, and the 
+border width to be 5. To draw the border correctly, we cannot simply 
+add/subtract 5 from the inner rectangle coordinates; if the user zooms 
+in by a factor of 2 then the border would become 10 px wide.
+
+Another way to say this is that a vector with length=1 in Visual coordinates
+does not _necessarily_ have a length of 1 pixel on the canvas. Instead, we must
+make use of the Document coordinate system, in which a vector of length=1
+does correspond to 1 pixel.
+
+There are a few ways we could make this measurement of pixel length. Here's
+how we'll do it in this tutorial:
+
+    1. Begin with vertices for a rectangle with border width 0 (that is, vertex
+       1 is the same as vertex 2, 3=4, and so on).
+    2. In the vertex shader, first map the vertices to the document coordinate
+       system using the visual->document transform.
+    3. Add/subtract the line width from the mapped vertices.
+    4. Map the rest of the way to render coordinates with a second transform:
+       document->framebuffer->render.
+
+Note that this problem _cannot_ be solved using a simple scale factor! It is
+necessary to use these transformations in order to draw correctly when there
+is rotation or anosotropic scaling involved.
+
+"""
+from vispy import app, gloo, visuals, scene
+import numpy as np
+
+
+vertex_shader = """
+void main() {
+    // First map the vertex to document coordinates
+    vec4 doc_pos = $visual_to_doc(vec4($position, 0, 1));
+    
+    // Also need to map the adjustment direction vector, but this is tricky!
+    // We need to adjust separately for each component of the vector:
+    vec4 adjusted;
+    if ( $adjust_dir.x == 0 ) {
+        // If this is an outer vertex, no adjustment for line weight is needed.
+        // (In fact, trying to make the adjustment would result in no
+        // triangles being drawn, hence the if/else block)
+        adjusted = doc_pos;
+    }
+    else {
+        // Inner vertexes must be adjusted for line width, but this is
+        // surprisingly tricky given that the rectangle may have been scaled 
+        // and rotated!
+        vec4 doc_x = $visual_to_doc(vec4($adjust_dir.x, 0, 0, 0)) - 
+                    $visual_to_doc(vec4(0, 0, 0, 0));
+        vec4 doc_y = $visual_to_doc(vec4(0, $adjust_dir.y, 0, 0)) - 
+                    $visual_to_doc(vec4(0, 0, 0, 0));
+        doc_x = normalize(doc_x);
+        doc_y = normalize(doc_y);
+                        
+        // Now doc_x + doc_y points in the direction we need in order to 
+        // correct the line weight of _both_ segments, but the magnitude of
+        // that correction is wrong. To correct it we first need to 
+        // measure the width that would result from using doc_x + doc_y:
+        vec4 proj_y_x = dot(doc_x, doc_y) * doc_x;  // project y onto x
+        float cur_width = length(doc_y - proj_y_x);  // measure current weight
+        
+        // And now we can adjust vertex position for line width:
+        adjusted = doc_pos + ($line_width / cur_width) * (doc_x + doc_y);
+    }
+    
+    // Finally map the remainder of the way to render coordinates
+    gl_Position = $doc_to_render(adjusted);
+}
+"""
+
+fragment_shader = """
+void main() {
+    gl_FragColor = $color;
+}
+"""
+
+
+class MyRectVisual(visuals.Visual):
+    """Visual that draws a rectangular outline.
+    
+    Parameters
+    ----------
+    x : float
+        x coordinate of rectangle origin
+    y : float
+        y coordinate of rectangle origin
+    w : float
+        width of rectangle
+    h : float
+        height of rectangle
+    weight : float
+        width of border (in px)
+    """
+    
+    def __init__(self, x, y, w, h, weight=4.0):
+        visuals.Visual.__init__(self)
+        
+        # 10 vertices for 8 triangles (using triangle_strip) forming a 
+        # rectangular outline
+        self.vert_buffer = gloo.VertexBuffer(np.array([
+            [x, y], 
+            [x, y], 
+            [x+w, y], 
+            [x+w, y], 
+            [x+w, y+h],
+            [x+w, y+h],
+            [x, y+h],
+            [x, y+h],
+            [x, y], 
+            [x, y], 
+        ], dtype=np.float32))
+        
+        # Direction each vertex should move to correct for line width
+        # (the length of this vector will be corrected in the shader)
+        self.adj_buffer = gloo.VertexBuffer(np.array([
+            [0, 0],
+            [1, 1],
+            [0, 0],
+            [-1, 1],
+            [0, 0],
+            [-1, -1],
+            [0, 0],
+            [1, -1],
+            [0, 0],
+            [1, 1],
+        ], dtype=np.float32))
+        
+        self.program = visuals.shaders.ModularProgram(vertex_shader, 
+                                                      fragment_shader)
+        
+        self.program.vert['position'] = self.vert_buffer
+        self.program.vert['adjust_dir'] = self.adj_buffer
+        self.program.vert['line_width'] = weight
+        self.program.frag['color'] = (1, 0, 0, 1)
+        
+    def draw(self, transforms):
+        gloo.set_state(cull_face=False)
+        
+        # Set the two transforms required by the vertex shader:
+        self.program.vert['visual_to_doc'] = transforms.visual_to_document
+        self.program.vert['doc_to_render'] = (
+            transforms.framebuffer_to_render *
+            transforms.document_to_framebuffer) 
+        
+        # Finally, draw the triangles.
+        self.program.draw('triangle_strip')
+
+
+# As in the previous tutorial, we auto-generate a Visual+Node class for use
+# in the scenegraph.
+MyRect = scene.visuals.create_visual_node(MyRectVisual)
+
+
+# Finally we will test the visual by displaying in a scene.
+
+canvas = scene.SceneCanvas(keys='interactive', show=True)
+
+# This time we add a ViewBox to let the user zoom/pan
+view = canvas.central_widget.add_view()
+view.camera = 'panzoom'
+view.camera.rect = (0, 0, 800, 800)
+
+# ..and add the rects to the view instead of canvas.scene
+rects = [MyRect(100, 100, 200, 300, parent=view.scene),
+         MyRect(500, 100, 200, 300, parent=view.scene)]
+
+# Again, rotate one rectangle to ensure the transforms are working as we 
+# expect.
+tr = visuals.transforms.AffineTransform()
+tr.rotate(25, (0, 0, 1))
+rects[1].transform = tr
+
+# Add some text instructions
+text = scene.visuals.Text("Drag right mouse button to zoom.", 
+                          color='w',
+                          anchor_x='left',
+                          parent=view,
+                          pos=(20, 30))
+
+# ..and optionally start the event loop
+if __name__ == '__main__':
+    import sys
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/examples/tutorial/visuals/T03_antialiasing.py b/examples/tutorial/visuals/T03_antialiasing.py
new file mode 100644
index 0000000..2a7872f
--- /dev/null
+++ b/examples/tutorial/visuals/T03_antialiasing.py
@@ -0,0 +1,216 @@
+"""
+Tutorial: Creating Visuals
+==========================
+
+03. Antialiasing
+----------------
+
+In [tutorial 1] we learned how to draw a simple rectangle, and in [tutorial 2]
+we expanded on this by using the Document coordinate system to draw a 
+rectangular border of a specific width. In this tutorial we introduce the
+Framebuffer coordinate system, which is used for antialiasing measurements. 
+
+In order to antialias our edges, we need to introduce a calculation to the
+fragment shader that computes, for each pixel being drawn, the fraction of the 
+pixel that is covered by the visual's geometry. At first glance, it may seem
+that the Document coordinate system is sufficient for this purpose because it
+has unit-length pixels. However, there are two situations when the actual 
+pixels being filled by the fragment shader are not the same size as the pixels
+on the canvas:
+
+    1. High-resolution displays (such as retina displays) that report a canvas
+       resolution smaller than the actual framebuffer resolution.
+    2. When exporting to an image with a different size than the canvas.
+
+In most cases the discrepancy between Document and Framebuffer coordinates can
+be corrected by a simple scale factor. However, this fails for some interesting
+corner cases where the transform is more complex, such as in VR applications 
+using optical distortion correction. Decide for yourself: is this Visual for 
+my personal use, or is it intended for a broader audience? For simplicity in 
+this example, we will use a simple scale factor.
+"""
+
+from vispy import app, gloo, visuals, scene
+import numpy as np
+
+
+# Here we use almost the same vertex shader as in tutorial 2.
+# The important difference is the addition of the line_pos variable,
+# which measures position across the width of the border line.
+vertex_shader = """
+varying float line_pos;  // how far we are across the border line
+
+void main() {
+    // First map the vertex to document coordinates
+    vec4 doc_pos = $visual_to_doc(vec4($position, 0, 1));
+    
+    vec4 adjusted;
+    if ( $adjust_dir.x == 0 ) {
+        adjusted = doc_pos;
+        line_pos = $line_width;  // at the outside of the border
+    }
+    else {
+        // Inner vertexes must be adjusted for line width
+        vec4 doc_x = $visual_to_doc(vec4($adjust_dir.x, 0, 0, 0)) - 
+                    $visual_to_doc(vec4(0, 0, 0, 0));
+        vec4 doc_y = $visual_to_doc(vec4(0, $adjust_dir.y, 0, 0)) - 
+                    $visual_to_doc(vec4(0, 0, 0, 0));
+        doc_x = normalize(doc_x);
+        doc_y = normalize(doc_y);
+                        
+        vec4 proj_y_x = dot(doc_x, doc_y) * doc_x;  // project y onto x
+        float cur_width = length(doc_y - proj_y_x);  // measure current weight
+        
+        // And now we can adjust vertex position for line width:
+        adjusted = doc_pos + ($line_width / cur_width) * (doc_x + doc_y);
+        
+        line_pos = 0;  // at the inside of the border
+    }
+    
+    // Finally map the remainder of the way to render coordinates
+    gl_Position = $doc_to_render(adjusted);
+}
+"""
+
+# The fragment shader is updated to change the opacity of the color based on
+# the amount of the fragment that is covered by the visual's geometry.
+fragment_shader = """
+varying float line_pos;
+
+void main() {
+    // Decrease the alpha linearly as we come within 1 pixel of the edge.
+    // Note: this only approximates the actual fraction of the pixel that is
+    // covered by the visual's geometry. A more accurate measurement would
+    // produce better antialiasing, but the effect would be subtle.
+    float alpha = 1.0;
+    if ((line_pos * $doc_fb_scale) < 1) {
+        alpha = $color.a * line_pos;
+    }
+    else if ((line_pos * $doc_fb_scale) > ($line_width - 1)) {
+        alpha = $color.a * ($line_width - line_pos);
+    }
+    gl_FragColor = vec4($color.rgb, alpha);
+}
+"""
+
+
+# The visual class is defined almost exactly as in [tutorial 2]. The only 
+# major difference is that the draw() method now calculates a scale factor
+# for converting between document and framebuffer coordinates.
+class MyRectVisual(visuals.Visual):
+    """Visual that draws a rectangular outline.
+    
+    Parameters
+    ----------
+    x : float
+        x coordinate of rectangle origin
+    y : float
+        y coordinate of rectangle origin
+    w : float
+        width of rectangle
+    h : float
+        height of rectangle
+    weight : float
+        width of border (in px)
+    """
+    
+    def __init__(self, x, y, w, h, weight=4.0):
+        visuals.Visual.__init__(self)
+        self.weight = weight
+        
+        # 10 vertices for 8 triangles (using triangle_strip) forming a 
+        # rectangular outline
+        self.vert_buffer = gloo.VertexBuffer(np.array([
+            [x, y], 
+            [x, y], 
+            [x+w, y], 
+            [x+w, y], 
+            [x+w, y+h],
+            [x+w, y+h],
+            [x, y+h],
+            [x, y+h],
+            [x, y], 
+            [x, y], 
+        ], dtype=np.float32))
+        
+        # Direction each vertex should move to correct for line width
+        # (the length of this vector will be corrected in the shader)
+        self.adj_buffer = gloo.VertexBuffer(np.array([
+            [0, 0],
+            [1, 1],
+            [0, 0],
+            [-1, 1],
+            [0, 0],
+            [-1, -1],
+            [0, 0],
+            [1, -1],
+            [0, 0],
+            [1, 1],
+        ], dtype=np.float32))
+        
+        self.program = visuals.shaders.ModularProgram(vertex_shader, 
+                                                      fragment_shader)
+        
+        self.program.vert['position'] = self.vert_buffer
+        self.program.vert['adjust_dir'] = self.adj_buffer
+        # To compensate for antialiasing, add 1 to border width:
+        self.program.vert['line_width'] = weight + 1
+        self.program.frag['color'] = (1, 0, 0, 1)
+        
+    def draw(self, transforms):
+        gloo.set_state(cull_face=False)
+        
+        # Set the two transforms required by the vertex shader:
+        self.program.vert['visual_to_doc'] = transforms.visual_to_document
+        self.program.vert['doc_to_render'] = (
+            transforms.framebuffer_to_render *
+            transforms.document_to_framebuffer) 
+        
+        # Set the scale factor between document and framebuffer coordinate
+        # systems. This assumes a simple linear / isotropic scale; more complex
+        # transforms will yield strange results!
+        fbs = np.linalg.norm(transforms.document_to_framebuffer.map([1, 0]) -
+                             transforms.document_to_framebuffer.map([0, 0]))
+        self.program.frag['doc_fb_scale'] = fbs
+        self.program.frag['line_width'] = (self.weight + 1) * fbs
+        
+        # Finally, draw the triangles.
+        self.program.draw('triangle_strip')
+
+
+# As in the previous tutorial, we auto-generate a Visual+Node class for use
+# in the scenegraph.
+MyRect = scene.visuals.create_visual_node(MyRectVisual)
+
+
+# Finally we will test the visual by displaying in a scene.
+
+canvas = scene.SceneCanvas(keys='interactive', show=True)
+
+# This time we add a ViewBox to let the user zoom/pan
+view = canvas.central_widget.add_view()
+view.camera = 'panzoom'
+view.camera.rect = (0, 0, 800, 800)
+
+# ..and add the rects to the view instead of canvas.scene
+rects = [MyRect(100, 100, 200, 300, parent=view.scene),
+         MyRect(500, 100, 200, 300, parent=view.scene)]
+
+# Again, rotate one rectangle to ensure the transforms are working as we 
+# expect.
+tr = visuals.transforms.AffineTransform()
+tr.rotate(25, (0, 0, 1))
+rects[1].transform = tr
+
+# Add some text instructions
+text = scene.visuals.Text("Drag right mouse button to zoom.", 
+                          color='w',
+                          anchor_x='left',
+                          parent=view,
+                          pos=(20, 30))
+
+# ..and optionally start the event loop
+if __name__ == '__main__':
+    import sys
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/examples/tutorial/visuals/T04_fragment_programs.py b/examples/tutorial/visuals/T04_fragment_programs.py
new file mode 100644
index 0000000..a07278f
--- /dev/null
+++ b/examples/tutorial/visuals/T04_fragment_programs.py
@@ -0,0 +1,81 @@
+"""
+Tutorial: Creating Visuals
+==========================
+
+04. Fragment Programs
+---------------------
+
+In this tutorial, we will demonstrate the use of the fragment shader as a 
+raycaster to draw complex shapes on a simple rectanglular mesh.
+
+Previous tutorials focused on the use of forward transformation functions to 
+map vertices from the local coordinate system of the visual to the "render 
+coordinates" output of the vertex shader. In this tutorial, we will use inverse
+transformation functions in the fragment shader to map backward from the 
+current fragment location to the visual's local coordinate system. 
+"""
+import numpy as np
+from vispy import app, gloo, visuals, scene
+
+
+vertex_shader = """
+void main() {
+   gl_Position = vec4($position, 0, 1);
+}
+"""
+
+fragment_shader = """
+void main() {
+  vec4 pos = $fb_to_visual(gl_FragCoord);
+  gl_FragColor = vec4(sin(pos.x / 10.), sin(pos.y / 10.), 0, 1);
+}
+"""
+
+
+class MyRectVisual(visuals.Visual):
+    """
+    """
+    
+    def __init__(self):
+        visuals.Visual.__init__(self)
+        self.vbo = gloo.VertexBuffer(np.array([
+            [-1, -1], [1, -1], [1, 1],
+            [-1, -1], [1, 1], [-1, 1]
+        ], dtype=np.float32))
+        self.program = visuals.shaders.ModularProgram(vertex_shader, 
+                                                      fragment_shader)
+        self.program.vert['position'] = self.vbo
+        
+    def draw(self, transforms):
+        gloo.set_state(cull_face=False)
+        
+        tr = (transforms.visual_to_document * 
+              transforms.document_to_framebuffer).inverse
+        self.program.frag['fb_to_visual'] = tr
+                
+        # Finally, draw the triangles.
+        self.program.draw('triangle_fan')
+
+
+# As in the previous tutorial, we auto-generate a Visual+Node class for use
+# in the scenegraph.
+MyRect = scene.visuals.create_visual_node(MyRectVisual)
+
+
+# Finally we will test the visual by displaying in a scene.
+
+canvas = scene.SceneCanvas(keys='interactive', show=True)
+
+# This time we add a ViewBox to let the user zoom/pan
+view = canvas.central_widget.add_view()
+view.camera = 'panzoom'
+view.camera.rect = (0, 0, 800, 800)
+
+vis = MyRect()
+view.add(vis)
+
+# ..and optionally start the event loop
+if __name__ == '__main__':
+    import sys
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/examples/tutorial/visuals/T05_viewer_location.py b/examples/tutorial/visuals/T05_viewer_location.py
new file mode 100644
index 0000000..4eab51e
--- /dev/null
+++ b/examples/tutorial/visuals/T05_viewer_location.py
@@ -0,0 +1,97 @@
+"""
+Tutorial: Creating Visuals
+==========================
+
+05. Camera location
+-------------------
+
+In this tutorial we will demonstrate how to determine the direction from which
+a Visual is being viewed.
+"""
+
+from vispy import app, gloo, visuals, scene, io
+
+
+vertex_shader = """
+varying vec4 color;
+void main() {
+    vec4 visual_pos = vec4($position, 1);
+    vec4 doc_pos = $visual_to_doc(visual_pos);
+    gl_Position = $doc_to_render(doc_pos);
+    
+    vec4 visual_pos2 = $doc_to_visual(doc_pos + vec4(0, 0, -1, 0));
+    vec4 view_direction = (visual_pos2 / visual_pos2.w) - visual_pos;
+    view_direction = vec4(normalize(view_direction.xyz), 0);
+    
+    color = vec4(view_direction.rgb, 1);
+}
+"""
+
+fragment_shader = """
+varying vec4 color;
+void main() {
+    gl_FragColor = color;
+}
+"""
+
+
+class MyMeshVisual(visuals.Visual):
+    """
+    """
+    
+    def __init__(self):
+        visuals.Visual.__init__(self)
+        
+        # Create an interesting mesh shape for demonstration.
+        fname = io.load_data_file('orig/triceratops.obj.gz')
+        vertices, faces, normals, tex = io.read_mesh(fname)
+        
+        self._ibo = gloo.IndexBuffer(faces)
+        
+        self.program = visuals.shaders.ModularProgram(vertex_shader, 
+                                                      fragment_shader)
+        self.program.vert['position'] = gloo.VertexBuffer(vertices)
+        #self.program.vert['normal'] = gloo.VertexBuffer(normals)
+        
+    def draw(self, transforms):
+        # Note we use the "additive" GL blending settings so that we do not 
+        # have to sort the mesh triangles back-to-front before each draw.
+        gloo.set_state('additive', cull_face=False)
+        
+        self.program.vert['visual_to_doc'] = transforms.visual_to_document
+        imap = transforms.visual_to_document.inverse
+        self.program.vert['doc_to_visual'] = imap
+        self.program.vert['doc_to_render'] = (
+            transforms.framebuffer_to_render * 
+            transforms.document_to_framebuffer)
+        
+        # Finally, draw the triangles.
+        self.program.draw('triangles', self._ibo)
+
+
+# Auto-generate a Visual+Node class for use in the scenegraph.
+MyMesh = scene.visuals.create_visual_node(MyMeshVisual)
+
+
+# Finally we will test the visual by displaying in a scene.
+
+canvas = scene.SceneCanvas(keys='interactive', show=True)
+
+# Add a ViewBox to let the user zoom/rotate
+view = canvas.central_widget.add_view()
+view.camera = 'turntable'
+view.camera.fov = 50
+view.camera.distance = 2
+
+mesh = MyMesh(parent=view.scene)
+mesh.transform = visuals.transforms.AffineTransform()
+#mesh.transform.translate([-25, -25, -25])
+mesh.transform.rotate(90, (1, 0, 0))
+
+axis = scene.visuals.XYZAxis(parent=view.scene)
+
+# ..and optionally start the event loop
+if __name__ == '__main__':
+    import sys
+    if sys.flags.interactive != 1:
+        app.run()
diff --git a/make/install_python.ps1 b/make/install_python.ps1
new file mode 100644
index 0000000..f18a107
--- /dev/null
+++ b/make/install_python.ps1
@@ -0,0 +1,125 @@
+# Sample script to install Python and pip under Windows
+# Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner
+# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
+
+$MINICONDA_URL = "http://repo.continuum.io/miniconda/"
+$MESA_GL_URL = "https://github.com/vispy/demo-data/raw/master/mesa/"
+
+# Mesa DLLs found linked from:
+#     http://qt-project.org/wiki/Cross-compiling-Mesa-for-Windows
+# to:
+#     http://sourceforge.net/projects/msys2/files/REPOS/MINGW/x86_64/mingw-w64-x86_64-mesa-10.2.4-1-any.pkg.tar.xz/download
+
+function DownloadMesaOpenGL ($architecture) {
+    $webclient = New-Object System.Net.WebClient
+    $basedir = $pwd.Path + "\"
+    $filepath = $basedir + "opengl32.dll"
+    # Download and retry up to 3 times in case of network transient errors.
+    $url = $MESA_GL_URL + "opengl32_mingw_" + $architecture + ".dll"
+    Write-Host "Downloading" $url
+    $retry_attempts = 2
+    for($i=0; $i -lt $retry_attempts; $i++){
+        try {
+            $webclient.DownloadFile($url, $filepath)
+            break
+        }
+        Catch [Exception]{
+            Start-Sleep 1
+        }
+    }
+    if (Test-Path $filepath) {
+        Write-Host "File saved at" $filepath
+    } else {
+        # Retry once to get the error message if any at the last try
+        $webclient.DownloadFile($url, $filepath)
+    }
+}
+
+
+function DownloadMiniconda ($python_version, $platform_suffix) {
+    $webclient = New-Object System.Net.WebClient
+    if ($python_version -eq "3.4") {
+        $filename = "Miniconda3-3.7.0-Windows-" + $platform_suffix + ".exe"
+    } else {
+        $filename = "Miniconda-3.7.0-Windows-" + $platform_suffix + ".exe"
+    }
+    $url = $MINICONDA_URL + $filename
+
+    $basedir = $pwd.Path + "\"
+    $filepath = $basedir + $filename
+    if (Test-Path $filename) {
+        Write-Host "Reusing" $filepath
+        return $filepath
+    }
+
+    # Download and retry up to 3 times in case of network transient errors.
+    Write-Host "Downloading" $filename "from" $url
+    $retry_attempts = 2
+    for($i=0; $i -lt $retry_attempts; $i++){
+        try {
+            $webclient.DownloadFile($url, $filepath)
+            break
+        }
+        Catch [Exception]{
+            Start-Sleep 1
+        }
+    }
+    if (Test-Path $filepath) {
+        Write-Host "File saved at" $filepath
+    } else {
+        # Retry once to get the error message if any at the last try
+        $webclient.DownloadFile($url, $filepath)
+    }
+    return $filepath
+}
+
+
+function InstallMiniconda ($python_version, $architecture, $python_home) {
+    Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
+    if (Test-Path $python_home) {
+        Write-Host $python_home "already exists, skipping."
+        return $false
+    }
+    if ($architecture -eq "32") {
+        $platform_suffix = "x86"
+    } else {
+        $platform_suffix = "x86_64"
+    }
+    $filepath = DownloadMiniconda $python_version $platform_suffix
+    Write-Host "Installing" $filepath "to" $python_home
+    $install_log = $python_home + ".log"
+    $args = "/S /D=$python_home"
+    Write-Host $filepath $args
+    Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru
+    if (Test-Path $python_home) {
+        Write-Host "Python $python_version ($architecture) installation complete"
+    } else {
+        Write-Host "Failed to install Python in $python_home"
+        Get-Content -Path $install_log
+        Exit 1
+    }
+}
+
+
+function InstallMinicondaPip ($python_home) {
+    $pip_path = $python_home + "\Scripts\pip.exe"
+    $conda_path = $python_home + "\Scripts\conda.exe"
+    if (-not(Test-Path $pip_path)) {
+        Write-Host "Installing pip..."
+        $args = "install --yes pip"
+        Write-Host $conda_path $args
+        Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru
+    } else {
+        Write-Host "pip already installed."
+    }
+}
+
+
+function main () {
+    # Don't download mesa for now since AppVeyor is unreliable with it
+    # DownloadMesaOpenGL $env:PYTHON_ARCH
+    InstallMiniconda $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON
+    InstallMinicondaPip $env:PYTHON
+}
+
+main
diff --git a/make/make.py b/make/make.py
index d740af0..3193c2c 100644
--- a/make/make.py
+++ b/make/make.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for mo
 
 """
@@ -79,6 +79,7 @@ class Maker:
         from coverage import coverage
         cov = coverage(auto_data=False, branch=True, data_suffix=None,
                        source=['vispy'])  # should match testing/_coverage.py
+        cov.combine()
         cov.load()
         cov.html_report()
         print('Done, launching browser.')
@@ -153,7 +154,11 @@ class Maker:
         # Go
         if 'html' == arg:
             sphinx_clean(build_dir)
-            sphinx_build(WEBSITE_DIR, build_dir)
+            try:
+                sphinx_build(WEBSITE_DIR, build_dir)
+            except SystemExit as err:
+                if err.code:
+                    raise
             sphinx_copy_pages(html_dir, PAGES_DIR, PAGES_REPO)
         elif 'show' == arg:
             sphinx_show(PAGES_DIR)
@@ -168,19 +173,22 @@ class Maker:
     def test(self, arg):
         """ Run tests:
                 * full - run all tests
-                * nose - run nose tests (also for each backend)
-                * any backend name (e.g. pyside, pyqt4, glut, sdl2, etc.) -
+                * unit - run tests (also for each backend)
+                * any backend name (e.g. pyside, pyqt4, etc.) -
                   run tests for the given backend
                 * nobackend - run tests that do not require a backend
                 * extra - run extra tests (line endings and style)
                 * lineendings - test line ending consistency
                 * flake - flake style testing (PEP8 and more)
+                * examples - run all examples
+                * examples [examples paths] - run given examples
         """
         if not arg:
             return self.help('test')
         from vispy import test
         try:
-            test(*(arg.split()))
+            args = arg.split(' ')
+            test(args[0], ' '.join(args[1:]))
         except Exception as err:
             print(err)
             if not isinstance(err, RuntimeError):
@@ -268,7 +276,7 @@ class Maker:
             for line in lines[:10]:
                 if line.startswith('# vispy:') and 'gallery' in line:
                     # Get what frames to grab
-                    frames = line.split('gallery')[1].strip()
+                    frames = line.split('gallery')[1].split(',')[0].strip()
                     frames = frames or '0'
                     frames = [int(i) for i in frames.split(':')]
                     if not frames:
@@ -309,8 +317,11 @@ class Maker:
                 c = m.canvas  # scene examples
             elif hasattr(m, 'Canvas'):
                 c = m.Canvas()
+            elif hasattr(m, 'fig'):
+                c = m.fig
             else:
                 print('Ignore: %s, no canvas' % name)
+                continue
             c.events.draw.connect(grabscreenshot)
             # Show it and draw as many frames as needed
             with c:
diff --git a/setup.py b/setup.py
index c3f310a..05443e3 100644
--- a/setup.py
+++ b/setup.py
@@ -87,13 +87,37 @@ setup(
     platforms='any',
     provides=['vispy'],
     install_requires=['numpy'],
+    extras_requires={
+        'ipython-static': ['ipython'],
+        'ipython-vnc': ['ipython>=2'],
+        'ipython-webgl': ['ipython>=2', 'tornado'],
+        'pyglet': ['pyglet>=1.2'],
+        # 'pyqt4': [],  # Why is this on PyPI, but without downloads?
+        # 'pyqt5': [],  # Ditto.
+        'pyside': ['PySide'],
+        'sdl2': ['PySDL2'],
+        'wx': ['wxPython'],
+    },
     packages=package_tree('vispy'),
     package_dir={
         'vispy': 'vispy'},
     package_data={
         'vispy': [op.join('io', '_data', '*'),
                   op.join('html', 'static', 'js', '*'),
-                  op.join('app', 'tests', 'qt-designer.ui')]},
+                  op.join('app', 'tests', 'qt-designer.ui')
+                  ],
+
+        'vispy.glsl': ['*.vert','*.frag', "*.glsl"],
+        'vispy.glsl.antialias': ['*.vert','*.frag', "*.glsl"],
+        'vispy.glsl.arrows': ['*.vert','*.frag', "*.glsl"],
+        'vispy.glsl.collections': ['*.vert','*.frag', "*.glsl"],
+        'vispy.glsl.colormaps': ['*.vert','*.frag', "*.glsl"],
+        'vispy.glsl.markers': ['*.vert','*.frag', "*.glsl"],
+        'vispy.glsl.math': ['*.vert','*.frag', "*.glsl"],
+        'vispy.glsl.misc': ['*.vert','*.frag', "*.glsl"],
+        'vispy.glsl.transforms': ['*.vert','*.frag', "*.glsl"],
+
+                  },
     zip_safe=False,
     classifiers=[
         'Development Status :: 3 - Alpha',
@@ -108,7 +132,7 @@ setup(
         'Programming Language :: Python',
         'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
-        'Programming Language :: Python :: 3.2',
         'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
     ],
 )
diff --git a/vispy/__init__.py b/vispy/__init__.py
index a76c180..edc9a24 100644
--- a/vispy/__init__.py
+++ b/vispy/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -22,13 +22,11 @@ from __future__ import division
 __all__ = ['use', 'sys_info', 'set_log_level', 'test']
 
 # Definition of the version number
-version_info = 0, 3, 0, ''  # major, minor, patch, extra
+version_info = 0, 4, 0  # major, minor, patch, extra
 
 # Nice string for the version (mimic how IPython composes its version str)
 __version__ = '-'.join(map(str, version_info)).replace('-', '.', 2).strip('-')
 
-from .util import (_parse_command_line_arguments, config,  # noqa
-                   set_log_level, keys, sys_info, test)  # noqa
+from .util import config, set_log_level, keys, sys_info  # noqa
 from .util.wrappers import use  # noqa
-
-_parse_command_line_arguments()
+from .testing import test  # noqa
diff --git a/vispy/app/__init__.py b/vispy/app/__init__.py
index 6762f81..b907734 100644
--- a/vispy/app/__init__.py
+++ b/vispy/app/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -13,6 +13,6 @@ from __future__ import division
 from .application import Application  # noqa
 from ._default_app import use_app, create, run, quit, process_events  # noqa
 from .canvas import Canvas, MouseEvent, KeyEvent  # noqa
+from .inputhook import set_interactive  # noqa
 from .timer import Timer  # noqa
 from . import base  # noqa
-from ._config import get_default_config  # noqa
diff --git a/vispy/app/_config.py b/vispy/app/_config.py
deleted file mode 100644
index 430faa0..0000000
--- a/vispy/app/_config.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from copy import deepcopy
-
-_default_dict = dict(red_size=8, green_size=8, blue_size=8, alpha_size=8,
-                     depth_size=16, stencil_size=0, double_buffer=True,
-                     stereo=False, samples=0)
-
-
-def get_default_config():
-    """Get the default OpenGL context configuration
-
-    Returns
-    -------
-    config : dict
-        Dictionary of config values.
-    """
-    return deepcopy(_default_dict)
diff --git a/vispy/app/_default_app.py b/vispy/app/_default_app.py
index dfc5581..0c15a47 100644
--- a/vispy/app/_default_app.py
+++ b/vispy/app/_default_app.py
@@ -1,28 +1,33 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from .application import Application
 
 # Initialize default app
-# Only for use within *this* module. 
+# Only for use within *this* module.
 # One should always call use_app() to obtain the default app.
 default_app = None
 
 
-def use_app(backend_name=None):
+def use_app(backend_name=None, call_reuse=True):
     """ Get/create the default Application object
-    
+
     It is safe to call this function multiple times, as long as
     backend_name is None or matches the already selected backend.
-    
+
     Parameters
     ----------
     backend_name : str | None
         The name of the backend application to use. If not specified, Vispy
         tries to select a backend automatically. See ``vispy.use()`` for
         details.
-    
+    call_reuse : bool
+        Whether to call the backend's `reuse()` function (True by default).
+        Not implemented by default, but some backends need it. For example,
+        the notebook backends need to inject some JavaScript in a notebook as
+        soon as `use_app()` is called.
+
     """
     global default_app
 
@@ -33,6 +38,8 @@ def use_app(backend_name=None):
         if backend_name and backend_name.lower() not in names:
             raise RuntimeError('Can only select a backend once.')
         else:
+            if call_reuse:
+                default_app.reuse()
             return default_app  # Current backend matches backend_name
 
     # Create default app
@@ -43,21 +50,21 @@ def use_app(backend_name=None):
 def create():
     """Create the native application.
     """
-    use_app()
+    use_app(call_reuse=False)
     return default_app.create()
 
 
 def run():
     """Enter the native GUI event loop.
     """
-    use_app()
+    use_app(call_reuse=False)
     return default_app.run()
 
 
 def quit():
     """Quit the native GUI event loop.
     """
-    use_app()
+    use_app(call_reuse=False)
     return default_app.quit()
 
 
@@ -67,5 +74,5 @@ def process_events():
     If the mainloop is not running, this should be done regularly to
     keep the visualization interactive and to keep the event system going.
     """
-    use_app()
+    use_app(call_reuse=False)
     return default_app.process_events()
diff --git a/vispy/app/application.py b/vispy/app/application.py
index ff8616e..14dc162 100644
--- a/vispy/app/application.py
+++ b/vispy/app/application.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -12,18 +12,19 @@ from __future__ import division
 import os
 import sys
 
-from . import backends
+from . import backends, inputhook
 from .backends import CORE_BACKENDS, BACKEND_NAMES, BACKENDMAP, TRIED_BACKENDS
 from .. import config
 from .base import BaseApplicationBackend as ApplicationBackend  # noqa
 from ..util import logger
+from ..ext import six
 
 
 class Application(object):
     """Representation of the vispy application
 
     This wraps a native GUI application instance. Vispy has a default
-    instance of this class that can be created/obtained via 
+    instance of this class that can be created/obtained via
     `vispy.app.use_app()`.
 
     Parameters
@@ -32,21 +33,21 @@ class Application(object):
         The name of the backend application to use. If not specified,
         Vispy tries to select a backend automatically. See ``vispy.use()``
         for details.
-    
+
     Notes
     -----
     Upon creating an Application object, a backend is selected, but the
     native backend application object is only created when `create()`
     is called or `native` is used. The Canvas and Timer do this
     automatically.
-    
+
     """
 
     def __init__(self, backend_name=None):
         self._backend_module = None
         self._backend = None
         self._use(backend_name)
-    
+
     def __repr__(self):
         name = self.backend_name
         if not name:
@@ -82,10 +83,52 @@ class Application(object):
         # Ensure that the native app exists
         self.native
 
-    def run(self):
+    def is_interactive(self):
+        """ Determine if the user requested interactive mode.
+        """
+        # The Python interpreter sets sys.flags correctly, so use them!
+        if sys.flags.interactive:
+            return True
+
+        # IPython does not set sys.flags when -i is specified, so first
+        # check it if it is already imported.
+        if '__IPYTHON__' not in dir(six.moves.builtins):
+            return False
+
+        # Then we check the application singleton and determine based on
+        # a variable it sets.
+        try:            
+            from IPython.config.application import Application as App
+            return App.initialized() and App.instance().interact
+        except (ImportError, AttributeError):
+            return False
+
+    def run(self, allow_interactive=True):
         """ Enter the native GUI event loop.
+
+        Parameters
+        ----------
+        allow_interactive : bool
+            Is the application allowed to handle interactive mode for console
+            terminals?  By default, typing ``python -i main.py`` results in
+            an interactive shell that also regularly calls the VisPy event
+            loop.  In this specific case, the run() function will terminate
+            immediately and rely on the interpreter's input loop to be run
+            after script execution.
+        """
+
+        if allow_interactive and self.is_interactive():
+            inputhook.set_interactive(enabled=True, app=self)
+        else:
+            return self._backend._vispy_run()
+
+    def reuse(self):
+        """ Called when the application is reused in an interactive session.
+        This allow the backend to do stuff in the client when `use_app()` is
+        called multiple times by the user. For example, the notebook backends
+        need to inject JavaScript code as soon as `use_app()` is called.
         """
-        return self._backend._vispy_run()
+        return self._backend._vispy_reuse()
 
     def quit(self):
         """ Quit the native GUI event loop.
@@ -101,10 +144,11 @@ class Application(object):
     def _use(self, backend_name=None):
         """Select a backend by name. See class docstring for details.
         """
-        # See if we're in a specific testing mode
-        test_name = os.getenv('_VISPY_TESTING_TYPE', None)
-        if test_name not in BACKENDMAP:
-            test_name = None
+        # See if we're in a specific testing mode, if so DONT check to see
+        # if it's a valid backend. If it isn't, it's a good thing we
+        # get an error later because we should have decorated our test
+        # with requires_application()
+        test_name = os.getenv('_VISPY_TESTING_APP', None)
 
         # Check whether the given name is valid
         if backend_name is not None:
@@ -163,8 +207,8 @@ class Application(object):
                     msg = ('Although %s is already imported, the %s backend '
                            'could not\nbe used ("%s"). \nNote that running '
                            'multiple GUI toolkits simultaneously can cause '
-                           'side effects.' % 
-                           (native_module_name, name, str(mod.why_not))) 
+                           'side effects.' %
+                           (native_module_name, name, str(mod.why_not)))
                     logger.warning(msg)
                 else:
                     # Inform otherwise
@@ -175,7 +219,9 @@ class Application(object):
                 logger.debug('Selected backend %s' % module_name)
                 break
         else:
-            raise RuntimeError('Could not import any of the backends.')
+            raise RuntimeError('Could not import any of the backends. '
+                               'You need to install any of %s. We recommend '
+                               'PyQt' % [b[0] for b in CORE_BACKENDS])
 
         # Store classes for app backend and canvas backend
         self._backend = self.backend_module.ApplicationBackend()
diff --git a/vispy/app/backends/__init__.py b/vispy/app/backends/__init__.py
index 2a47b6a..1000e68 100644
--- a/vispy/app/backends/__init__.py
+++ b/vispy/app/backends/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ vispy.app.backends
@@ -14,20 +14,21 @@ imported. This stuff is mostly used in the Application.use method.
 # This is the order in which they are attempted to be imported.
 CORE_BACKENDS = [
     ('PyQt4', '_pyqt4', 'PyQt4'),
+    ('PyQt5', '_pyqt5', 'PyQt5'),
     ('PySide', '_pyside', 'PySide'),
     ('Pyglet', '_pyglet', 'pyglet'),
     ('Glfw', '_glfw', 'vispy.ext.glfw'),
     ('SDL2', '_sdl2', 'sdl2'),
     ('wx', '_wx', 'wx'),
     ('EGL', '_egl', 'vispy.ext.egl'),
-    ('Glut', '_glut', 'OpenGL.GLUT'),
 ]
 
 # Whereas core backends really represents libraries that can create a
 # canvas, the pseudo backends act more like a proxy.
 PSEUDO_BACKENDS = [
-    ('ipynb_vnc', '_ipynb_vnc', None),
-    ('ipynb_static', '_ipynb_static', None),
+    # ('ipynb_vnc', '_ipynb_vnc', None),
+    # ('ipynb_static', '_ipynb_static', None),
+    ('ipynb_webgl', '_ipynb_webgl', None),
     ('_test', '_test', 'vispy.app.backends._test'),  # add one that will fail
 ]
 
diff --git a/vispy/app/backends/_egl.py b/vispy/app/backends/_egl.py
index 78179f3..34e2985 100644
--- a/vispy/app/backends/_egl.py
+++ b/vispy/app/backends/_egl.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 vispy headless backend for egl.
@@ -11,7 +11,7 @@ import atexit
 from time import sleep
 
 from ..base import (BaseApplicationBackend, BaseCanvasBackend,
-                    BaseTimerBackend, BaseSharedContext)
+                    BaseTimerBackend)
 from ...util.ptime import time
 
 # -------------------------------------------------------------------- init ---
@@ -30,6 +30,7 @@ try:
     egl.eglInitialize(_EGL_DISPLAY)
     version = [egl.eglQueryString(_EGL_DISPLAY, x) for x in
                [egl.EGL_VERSION, egl.EGL_VENDOR, egl.EGL_CLIENT_APIS]]
+    version = [v.decode('utf-8') for v in version]
     version = version[0] + ' ' + version[1] + ': ' + version[2].strip()
     atexit.register(egl.eglTerminate, _EGL_DISPLAY)
 except Exception as exp:
@@ -67,15 +68,10 @@ capability = dict(  # things that can be set by the backend
     multi_window=True,
     scroll=False,
     parent=False,
+    always_on_top=False,
 )
 
 
-# ------------------------------------------------------- set_configuration ---
-
-class SharedContext(BaseSharedContext):
-    _backend = 'egl'
-
-
 # ------------------------------------------------------------- application ---
 
 class ApplicationBackend(BaseApplicationBackend):
@@ -126,24 +122,33 @@ class CanvasBackend(BaseCanvasBackend):
 
     """ EGL backend for Canvas abstract class."""
 
-    def __init__(self, **kwargs):
-        BaseCanvasBackend.__init__(self, capability, SharedContext)
-        title, size, position, show, vsync, resize, dec, fs, parent, context, \
-            vispy_canvas = self._process_backend_kwargs(kwargs)
-        # Create "window"
-        if isinstance(context, dict):
-            self._config = egl.eglChooseConfig(_EGL_DISPLAY)[0]
-            self._context = egl.eglCreateContext(_EGL_DISPLAY, self._config,
-                                                 None)
+    # args are for BaseCanvasBackend, kwargs are for us.
+    def __init__(self, *args, **kwargs):
+        BaseCanvasBackend.__init__(self, *args)
+        p = self._process_backend_kwargs(kwargs)
+        self._initialized = False
+
+        # Deal with context
+        p.context.shared.add_ref('egl', self)
+        if p.context.shared.ref is self:
+            # Store context information
+            self._native_config = egl.eglChooseConfig(_EGL_DISPLAY)[0]
+            self._native_context = egl.eglCreateContext(_EGL_DISPLAY,
+                                                        self._native_config,
+                                                        None)
         else:
-            self._config, self._context = context.value
+            # Reuse information from other context
+            self._native_config = p.context.shared.ref._native_config
+            self._native_context = p.context.shared.ref._native_context
 
         self._surface = None
-        self._vispy_set_size(*size)
+        self._vispy_set_size(*p.size)
         _VP_EGL_ALL_WINDOWS.append(self)
-        self._initialized = False
-        self._vispy_set_current()
-        self._vispy_canvas = vispy_canvas
+
+        # Init
+        self._initialized = True
+        self._vispy_canvas.set_current()
+        self._vispy_canvas.events.initialize()
 
     def _destroy_surface(self):
         if self._surface is not None:
@@ -154,47 +159,28 @@ class CanvasBackend(BaseCanvasBackend):
         if self._surface is not None:
             self._destroy_surface()
         attrib_list = (egl.EGL_WIDTH, w, egl.EGL_HEIGHT, h)
-        self._surface = egl.eglCreatePbufferSurface(_EGL_DISPLAY, self._config,
+        self._surface = egl.eglCreatePbufferSurface(_EGL_DISPLAY,
+                                                    self._native_config,
                                                     attrib_list)
         if self._surface == egl.EGL_NO_SURFACE:
             raise RuntimeError('Could not create rendering surface')
         self._size = (w, h)
         self._vispy_update()
 
-    @property
-    def _vispy_context(self):
-        """Context to return for sharing"""
-        return SharedContext((self._config, self._context))
-
-    ####################################
-    # Deal with events we get from vispy
-    @property
-    def _vispy_canvas(self):
-        """ The parent canvas/window """
-        return self._vispy_canvas_
-
-    @_vispy_canvas.setter
-    def _vispy_canvas(self, vc):
-        # Init events when the property is set by Canvas
-        self._vispy_canvas_ = vc
-        if vc is not None and not self._initialized:
-            self._initialized = True
-            self._vispy_set_current()
-            self._vispy_canvas.events.initialize()
-
     def _vispy_warmup(self):
         etime = time() + 0.25
         while time() < etime:
             sleep(0.01)
-            self._vispy_set_current()
+            self._vispy_canvas.set_current()
             self._vispy_canvas.app.process_events()
 
     def _vispy_set_current(self):
         if self._surface is None:
             return
         # Make this the current context
+        self._vispy_canvas.set_current()  # Mark canvs as current
         egl.eglMakeCurrent(_EGL_DISPLAY, self._surface, self._surface,
-                           self._context)
+                           self._native_context)
 
     def _vispy_swap_buffers(self):
         if self._surface is None:
@@ -230,7 +216,7 @@ class CanvasBackend(BaseCanvasBackend):
         # This is called by the processing app
         if self._vispy_canvas is None or self._surface is None:
             return
-        self._vispy_set_current()
+        self._vispy_canvas.set_current()
         self._vispy_canvas.events.draw(region=None)  # (0, 0, w, h))
 
 
diff --git a/vispy/app/backends/_glfw.py b/vispy/app/backends/_glfw.py
index a057af9..3c0d527 100644
--- a/vispy/app/backends/_glfw.py
+++ b/vispy/app/backends/_glfw.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 vispy backend for glfw.
@@ -21,11 +21,15 @@ from __future__ import division
 import atexit
 from time import sleep
 import gc
+import os
 
 from ..base import (BaseApplicationBackend, BaseCanvasBackend,
-                    BaseTimerBackend, BaseSharedContext)
+                    BaseTimerBackend)
 from ...util import keys, logger
 from ...util.ptime import time
+from ... import config
+
+USE_EGL = config['gl_backend'].lower().startswith('es')
 
 
 # -------------------------------------------------------------------- init ---
@@ -85,8 +89,12 @@ try:
 except Exception as exp:
     available, testable, why_not, which = False, False, str(exp), None
 else:
-    available, testable, why_not = True, True, None
-    which = 'glfw ' + str(glfw.__version__)
+    if USE_EGL:
+        available, testable, why_not = False, False, 'EGL not supported'
+        which = 'glfw ' + str(glfw.__version__)
+    else:
+        available, testable, why_not = True, True, None
+        which = 'glfw ' + str(glfw.__version__)
 
 MOD_KEYS = [keys.SHIFT, keys.ALT, keys.CONTROL, keys.META]
 _GLFW_INITIALIZED = False
@@ -116,6 +124,7 @@ capability = dict(  # things that can be set by the backend
     multi_window=True,
     scroll=True,
     parent=False,
+    always_on_top=True,
 )
 
 
@@ -135,9 +144,9 @@ def _set_config(c):
 
     glfw.glfwWindowHint(glfw.GLFW_DEPTH_BITS, c['depth_size'])
     glfw.glfwWindowHint(glfw.GLFW_STENCIL_BITS, c['stencil_size'])
-    #glfw.glfwWindowHint(glfw.GLFW_CONTEXT_VERSION_MAJOR, c['major_version'])
-    #glfw.glfwWindowHint(glfw.GLFW_CONTEXT_VERSION_MINOR, c['minor_version'])
-    #glfw.glfwWindowHint(glfw.GLFW_SRGB_CAPABLE, c['srgb'])
+    # glfw.glfwWindowHint(glfw.GLFW_CONTEXT_VERSION_MAJOR, c['major_version'])
+    # glfw.glfwWindowHint(glfw.GLFW_CONTEXT_VERSION_MINOR, c['minor_version'])
+    # glfw.glfwWindowHint(glfw.GLFW_SRGB_CAPABLE, c['srgb'])
     glfw.glfwWindowHint(glfw.GLFW_SAMPLES, c['samples'])
     glfw.glfwWindowHint(glfw.GLFW_STEREO, c['stereo'])
     if not c['double_buffer']:
@@ -145,11 +154,15 @@ def _set_config(c):
                            'different backend, or using double buffering')
 
 
-class SharedContext(BaseSharedContext):
-    _backend = 'glfw'
+# ------------------------------------------------------------- application ---
 
 
-# ------------------------------------------------------------- application ---
+_glfw_errors = []
+
+
+def _error_callback(num, descr):
+    _glfw_errors.append('Error %s: %s' % (num, descr))
+
 
 class ApplicationBackend(BaseApplicationBackend):
 
@@ -185,7 +198,8 @@ class ApplicationBackend(BaseApplicationBackend):
         # Close windows
         wins = _get_glfw_windows()
         for win in wins:
-            win._vispy_close()
+            if win._vispy_canvas is not None:
+                win._vispy_canvas.close()
         # tear down timers
         for timer in self._timers:
             timer._vispy_stop()
@@ -194,8 +208,14 @@ class ApplicationBackend(BaseApplicationBackend):
     def _vispy_get_native_app(self):
         global _GLFW_INITIALIZED
         if not _GLFW_INITIALIZED:
-            if not glfw.glfwInit():  # only ever call once
-                raise OSError('Could not init glfw')
+            cwd = os.getcwd()
+            glfw.glfwSetErrorCallback(_error_callback)
+            try:
+                if not glfw.glfwInit():  # only ever call once
+                    raise OSError('Could not init glfw:\n%r' % _glfw_errors)
+            finally:
+                os.chdir(cwd)
+            glfw.glfwSetErrorCallback(0)
             atexit.register(glfw.glfwTerminate)
             _GLFW_INITIALIZED = True
         return glfw
@@ -207,51 +227,59 @@ class CanvasBackend(BaseCanvasBackend):
 
     """ Glfw backend for Canvas abstract class."""
 
-    def __init__(self, **kwargs):
-        BaseCanvasBackend.__init__(self, capability, SharedContext)
-        title, size, position, show, vsync, resize, dec, fs, parent, context, \
-            vispy_canvas = self._process_backend_kwargs(kwargs)
-        # Init GLFW, add window hints, and create window
-        if isinstance(context, dict):
-            _set_config(context)
+    # args are for BaseCanvasBackend, kwargs are for us.
+    def __init__(self, *args, **kwargs):
+        BaseCanvasBackend.__init__(self, *args)
+        p = self._process_backend_kwargs(kwargs)
+        self._initialized = False
+
+        # Deal with config
+        _set_config(p.context.config)
+        # Deal with context
+        p.context.shared.add_ref('glfw', self)
+        if p.context.shared.ref is self:
             share = None
         else:
-            share = context.value
+            share = p.context.shared.ref._id
+
         glfw.glfwWindowHint(glfw.GLFW_REFRESH_RATE, 0)  # highest possible
-        glfw.glfwSwapInterval(1 if vsync else 0)
-        glfw.glfwWindowHint(glfw.GLFW_RESIZABLE, int(resize))
-        glfw.glfwWindowHint(glfw.GLFW_DECORATED, int(dec))
+        glfw.glfwSwapInterval(1 if p.vsync else 0)
+        glfw.glfwWindowHint(glfw.GLFW_RESIZABLE, int(p.resizable))
+        glfw.glfwWindowHint(glfw.GLFW_DECORATED, int(p.decorate))
         glfw.glfwWindowHint(glfw.GLFW_VISIBLE, 0)  # start out hidden
-        if fs is not False:
+        glfw.glfwWindowHint(glfw.GLFW_FLOATING, int(p.always_on_top))
+        if p.fullscreen is not False:
             self._fullscreen = True
-            if fs is True:
+            if p.fullscreen is True:
                 monitor = glfw.glfwGetPrimaryMonitor()
             else:
                 monitor = glfw.glfwGetMonitors()
-                if fs >= len(monitor):
+                if p.fullscreen >= len(monitor):
                     raise ValueError('fullscreen must be <= %s'
                                      % len(monitor))
-                monitor = monitor[fs]
+                monitor = monitor[p.fullscreen]
             use_size = glfw.glfwGetVideoMode(monitor)[:2]
-            if use_size != size:
-                logger.warning('Requested size %s, will be ignored to '
-                               'use fullscreen mode %s' % (size, use_size))
+            if use_size != tuple(p.size):
+                logger.debug('Requested size %s, will be ignored to '
+                             'use fullscreen mode %s' % (p.size, use_size))
             size = use_size
         else:
             self._fullscreen = False
             monitor = None
+            size = p.size
 
         self._id = glfw.glfwCreateWindow(width=size[0], height=size[1],
-                                         title=title, monitor=monitor,
+                                         title=p.title, monitor=monitor,
                                          share=share)
         if not self._id:
             raise RuntimeError('Could not create window')
+
         _VP_GLFW_ALL_WINDOWS.append(self)
         self._mod = list()
 
         # Register callbacks
         glfw.glfwSetWindowRefreshCallback(self._id, self._on_draw)
-        glfw.glfwSetFramebufferSizeCallback(self._id, self._on_resize)
+        glfw.glfwSetWindowSizeCallback(self._id, self._on_resize)
         glfw.glfwSetKeyCallback(self._id, self._on_key_press)
         glfw.glfwSetMouseButtonCallback(self._id, self._on_mouse_button)
         glfw.glfwSetScrollCallback(self._id, self._on_mouse_scroll)
@@ -259,40 +287,22 @@ class CanvasBackend(BaseCanvasBackend):
         glfw.glfwSetWindowCloseCallback(self._id, self._on_close)
         self._vispy_canvas_ = None
         self._needs_draw = False
-        self._vispy_set_current()
-        if position is not None:
-            self._vispy_set_position(*position)
-        if show:
+        self._vispy_canvas.set_current()
+        if p.position is not None:
+            self._vispy_set_position(*p.position)
+        if p.show:
             glfw.glfwShowWindow(self._id)
-        self._initialized = False
-        self._vispy_canvas = vispy_canvas
-
-    @property
-    def _vispy_context(self):
-        """Context to return for sharing"""
-        return SharedContext(self._id)
-
-    ####################################
-    # Deal with events we get from vispy
-    @property
-    def _vispy_canvas(self):
-        """ The parent canvas/window """
-        return self._vispy_canvas_
-
-    @_vispy_canvas.setter
-    def _vispy_canvas(self, vc):
-        # Init events when the property is set by Canvas
-        self._vispy_canvas_ = vc
-        if vc is not None and not self._initialized:
-            self._initialized = True
-            self._vispy_set_current()
-            self._vispy_canvas.events.initialize()
+
+        # Init
+        self._initialized = True
+        self._vispy_canvas.set_current()
+        self._vispy_canvas.events.initialize()
 
     def _vispy_warmup(self):
         etime = time() + 0.25
         while time() < etime:
             sleep(0.01)
-            self._vispy_set_current()
+            self._vispy_canvas.set_current()
             self._vispy_canvas.app.process_events()
 
     def _vispy_set_current(self):
@@ -353,13 +363,18 @@ class CanvasBackend(BaseCanvasBackend):
             # glfw.glfwSetWindowShouldClose()  # Does not really cause a close
             self._vispy_set_visible(False)
             self._id, id_ = None, self._id
-            glfw.glfwPollEvents()
             glfw.glfwDestroyWindow(id_)
             gc.collect()  # help ensure context gets destroyed
 
     def _vispy_get_size(self):
         if self._id is None:
             return
+        w, h = glfw.glfwGetWindowSize(self._id)
+        return w, h
+
+    def _vispy_get_physical_size(self):
+        if self._id is None:
+            return
         w, h = glfw.glfwGetFramebufferSize(self._id)
         return w, h
 
@@ -377,7 +392,8 @@ class CanvasBackend(BaseCanvasBackend):
     def _on_resize(self, _id, w, h):
         if self._vispy_canvas is None:
             return
-        self._vispy_canvas.events.resize(size=(w, h))
+        self._vispy_canvas.events.resize(
+            size=(w, h), physical_size=self._vispy_get_physical_size())
 
     def _on_close(self, _id):
         if self._vispy_canvas is None:
@@ -387,7 +403,7 @@ class CanvasBackend(BaseCanvasBackend):
     def _on_draw(self, _id=None):
         if self._vispy_canvas is None or self._id is None:
             return
-        self._vispy_set_current()
+        self._vispy_canvas.set_current()
         self._vispy_canvas.events.draw(region=None)  # (0, 0, w, h))
 
     def _on_mouse_button(self, _id, button, action, mod):
@@ -434,10 +450,10 @@ class CanvasBackend(BaseCanvasBackend):
         fun(key=key, text=text, modifiers=self._mod)
 
     def _process_key(self, key):
-        if key in KEYMAP:
-            return KEYMAP[key], ''
-        elif 32 <= key <= 127:
+        if 32 <= key <= 127:
             return keys.Key(chr(key)), chr(key)
+        elif key in KEYMAP:
+            return KEYMAP[key], ''
         else:
             return None, ''
 
diff --git a/vispy/app/backends/_glut.py b/vispy/app/backends/_glut.py
deleted file mode 100644
index afae608..0000000
--- a/vispy/app/backends/_glut.py
+++ /dev/null
@@ -1,502 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-"""
-vispy backend for glut.
-"""
-
-from __future__ import division
-
-import sys
-from time import sleep, time
-
-from ..base import (BaseApplicationBackend, BaseCanvasBackend,
-                    BaseTimerBackend, BaseSharedContext)
-from ...util import ptime, keys, logger
-
-# -------------------------------------------------------------------- init ---
-
-try:
-    import OpenGL.error
-    import OpenGL.GLUT as glut
-
-    # glut.GLUT_ACTIVE_SHIFT: keys.SHIFT,
-    # glut.GLUT_ACTIVE_CTRL: keys.CONTROL,
-    # glut.GLUT_ACTIVE_ALT: keys.ALT,
-    # -1: keys.META,
-
-    # Map native keys to vispy keys
-    KEYMAP = {
-        -1: keys.SHIFT,
-        -2: keys.CONTROL,
-        -3: keys.ALT,
-        -4: keys.META,
-
-        glut.GLUT_KEY_LEFT: keys.LEFT,
-        glut.GLUT_KEY_UP: keys.UP,
-        glut.GLUT_KEY_RIGHT: keys.RIGHT,
-        glut.GLUT_KEY_DOWN: keys.DOWN,
-        glut.GLUT_KEY_PAGE_UP: keys.PAGEUP,
-        glut.GLUT_KEY_PAGE_DOWN: keys.PAGEDOWN,
-
-        glut.GLUT_KEY_INSERT: keys.INSERT,
-        chr(127): keys.DELETE,
-        glut.GLUT_KEY_HOME: keys.HOME,
-        glut.GLUT_KEY_END: keys.END,
-
-        chr(27): keys.ESCAPE,
-        chr(8): keys.BACKSPACE,
-
-        glut.GLUT_KEY_F1: keys.F1,
-        glut.GLUT_KEY_F2: keys.F2,
-        glut.GLUT_KEY_F3: keys.F3,
-        glut.GLUT_KEY_F4: keys.F4,
-        glut.GLUT_KEY_F5: keys.F5,
-        glut.GLUT_KEY_F6: keys.F6,
-        glut.GLUT_KEY_F7: keys.F7,
-        glut.GLUT_KEY_F8: keys.F8,
-        glut.GLUT_KEY_F9: keys.F9,
-        glut.GLUT_KEY_F10: keys.F10,
-        glut.GLUT_KEY_F11: keys.F11,
-        glut.GLUT_KEY_F12: keys.F12,
-
-        ' ': keys.SPACE,
-        '\r': keys.ENTER,
-        '\n': keys.ENTER,
-        '\t': keys.TAB,
-    }
-
-    BUTTONMAP = {glut.GLUT_LEFT_BUTTON: 1,
-                 glut.GLUT_RIGHT_BUTTON: 2,
-                 glut.GLUT_MIDDLE_BUTTON: 3
-                 }
-
-    def _get_glut_process_func():
-        if hasattr(glut, 'glutMainLoopEvent') and bool(glut.glutMainLoopEvent):
-            func = glut.glutMainLoopEvent
-        elif hasattr(glut, 'glutCheckLoop') and bool(glut.glutCheckLoop):
-            func = glut.glutCheckLoop  # Darwin
-        else:
-            msg = ('Your implementation of GLUT does not allow '
-                   'interactivity. Consider installing freeglut.')
-            raise RuntimeError(msg)
-        return func
-except Exception as exp:
-    available, testable, why_not, which = False, False, str(exp), None
-else:
-    available, why_not, testable = True, None, True
-    try:
-        _get_glut_process_func()
-    except RuntimeError:
-        testable, why_not = False, 'No process_func'
-    which = 'from OpenGL %s' % OpenGL.__version__
-
-
-_GLUT_INITIALIZED = False
-_VP_GLUT_ALL_WINDOWS = []
-
-# -------------------------------------------------------------- capability ---
-
-capability = dict(  # things that can be set by the backend
-    title=True,
-    size=True,
-    position=True,
-    show=True,
-    vsync=False,
-    resizable=False,
-    decorate=False,
-    fullscreen=True,
-    context=False,
-    multi_window=False,
-    scroll=False,
-    parent=False,
-)
-
-
-# ------------------------------------------------------- set_configuration ---
-
-def _set_config(config):
-    """Set gl configuration"""
-    s = ""
-    st = '~' if sys.platform == 'darwin' else '='
-    ge = '>=' if sys.platform == 'darwin' else '='
-    s += "red%s%d " % (ge, config['red_size'])
-    s += "green%s%d " % (ge, config['green_size'])
-    s += "blue%s%d " % (ge, config['blue_size'])
-    s += "alpha%s%d " % (ge, config['alpha_size'])
-    s += "depth%s%d " % (ge, config['depth_size'])
-    s += "stencil%s%d " % (st, config['stencil_size'])
-    s += "samples%s%d " % (st, config['samples']) if config['samples'] else ""
-    s += "acca=0 " if sys.platform == 'darwin' else ""
-    if sys.platform == 'darwin':
-        s += "double=1 " if config['double_buffer'] else "single=1 "
-        s += "stereo=%d " % config['stereo']
-    else:  # freeglut
-        s += "double " if config['double_buffer'] else "single "
-        s += "stereo " if config['stereo'] else ""
-    glut.glutInitDisplayString(s.encode('ASCII'))
-
-
-class SharedContext(BaseSharedContext):
-    _backend = 'glut'
-
-
-# ------------------------------------------------------------- application ---
-
-class ApplicationBackend(BaseApplicationBackend):
-
-    def __init__(self):
-        BaseApplicationBackend.__init__(self)
-        self._timers = []
-
-    def _add_timer(self, timer):
-        if timer not in self._timers:
-            self._timers.append(timer)
-
-    def _vispy_get_backend_name(self):
-        return 'Glut'
-
-    def _vispy_process_events(self):
-        # Determine what function to use, if any
-        try:
-            func = _get_glut_process_func()
-        except RuntimeError:
-            self._vispy_process_events = lambda: None
-            raise
-        # Set for future use, and call!
-        self._proc_fun = func
-        self._vispy_process_events = self._process_events_and_timer
-        self._process_events_and_timer()
-
-    def _process_events_and_timer(self):
-        # helper to both call glutMainLoopEvent and tick the timers
-        self._proc_fun()
-        for timer in self._timers:
-            timer._idle_callback()
-
-    def _vispy_run(self):
-        self._vispy_get_native_app()  # Force exist
-        return glut.glutMainLoop()
-
-    def _vispy_quit(self):
-        for timer in self._timers:
-            timer._vispy_stop()
-        self._timers = []
-        if hasattr(glut, 'glutLeaveMainLoop') and bool(glut.glutLeaveMainLoop):
-            glut.glutLeaveMainLoop()
-        else:
-            for win in _VP_GLUT_ALL_WINDOWS:
-                win._vispy_close()
-
-    def _vispy_get_native_app(self):
-        global _GLUT_INITIALIZED
-        if not _GLUT_INITIALIZED:
-            glut.glutInit(['vispy'.encode('ASCII')])
-            # Prevent exit when closing window
-            try:
-                glut.glutSetOption(glut.GLUT_ACTION_ON_WINDOW_CLOSE,
-                                   glut.GLUT_ACTION_CONTINUE_EXECUTION)
-            except Exception:
-                pass
-            _GLUT_INITIALIZED = True
-        return glut
-
-
-def _set_close_fun(id_, fun):
-    # Set close function. See issue #10. For some reason, the function
-    # can still not exist even if we checked its boolean status.
-    glut.glutSetWindow(id_)
-    closeFuncSet = False
-    if bool(glut.glutWMCloseFunc):  # OSX specific test
-        try:
-            glut.glutWMCloseFunc(fun)
-            closeFuncSet = True
-        except OpenGL.error.NullFunctionError:
-            pass
-    if not closeFuncSet:
-        try:
-            glut.glutCloseFunc(fun)
-            closeFuncSet = True
-        except OpenGL.error.NullFunctionError:
-            pass
-
-
-# ------------------------------------------------------------------ canvas ---
-
-class CanvasBackend(BaseCanvasBackend):
-
-    """ GLUT backend for Canvas abstract class."""
-
-    def __init__(self, **kwargs):
-        BaseCanvasBackend.__init__(self, capability, SharedContext)
-        title, size, position, show, vsync, resize, dec, fs, parent, context, \
-            vispy_canvas = self._process_backend_kwargs(kwargs)
-        _set_config(context)
-        glut.glutInitWindowSize(size[0], size[1])
-        self._id = glut.glutCreateWindow(title.encode('ASCII'))
-        if not self._id:
-            raise RuntimeError('could not create window')
-        glut.glutSetWindow(self._id)
-        _VP_GLUT_ALL_WINDOWS.append(self)
-        if fs is not False:
-            self._fullscreen = True
-            self._old_size = size
-            if fs is not True:
-                logger.warning('Cannot specify monitor for glut fullscreen, '
-                               'using default')
-            glut.glutFullScreen()
-        else:
-            self._fullscreen = False
-
-        # Cache of modifiers so we can send modifiers along with mouse motion
-        self._modifiers_cache = ()
-        self._closed = False  # Keep track whether the widget is closed
-
-        # Register callbacks
-        glut.glutDisplayFunc(self.on_draw)
-        glut.glutReshapeFunc(self.on_resize)
-        # glut.glutVisibilityFunc(self.on_show)
-        glut.glutKeyboardFunc(self.on_key_press)
-        glut.glutSpecialFunc(self.on_key_press)
-        glut.glutKeyboardUpFunc(self.on_key_release)
-        glut.glutMouseFunc(self.on_mouse_action)
-        glut.glutMotionFunc(self.on_mouse_motion)
-        glut.glutPassiveMotionFunc(self.on_mouse_motion)
-        _set_close_fun(self._id, self.on_close)
-        if position is not None:
-            self._vispy_set_position(*position)
-        if not show:
-            glut.glutHideWindow()
-        self._initialized = False
-        self._vispy_canvas = vispy_canvas
-
-    @property
-    def _vispy_context(self):
-        """Context to return for sharing"""
-        return SharedContext(None)  # cannot share in GLUT
-
-    def _vispy_warmup(self):
-        etime = time() + 0.4  # empirically determined :(
-        while time() < etime:
-            sleep(0.01)
-            self._vispy_set_current()
-            self._vispy_canvas.app.process_events()
-
-    @property
-    def _vispy_canvas(self):
-        """ The parent canvas/window """
-        return self._vispy_canvas_
-
-    @_vispy_canvas.setter
-    def _vispy_canvas(self, vc):
-        # Init events when the property is set by Canvas
-        self._vispy_canvas_ = vc
-        if vc is not None and not self._initialized:
-            self._initialized = True
-            self._vispy_set_current()
-            self._vispy_canvas.events.initialize()
-
-    def _vispy_set_current(self):
-        # Make this the current context
-        glut.glutSetWindow(self._id)
-
-    def _vispy_swap_buffers(self):
-        # Swap front and back buffer
-        glut.glutSetWindow(self._id)
-        glut.glutSwapBuffers()
-
-    def _vispy_set_title(self, title):
-        # Set the window title. Has no effect for widgets
-        glut.glutSetWindow(self._id)
-        glut.glutSetWindowTitle(title.encode('ASCII'))
-
-    def _vispy_set_size(self, w, h):
-        # Set size of the widget or window
-        glut.glutSetWindow(self._id)
-        glut.glutReshapeWindow(w, h)
-
-    def _vispy_set_position(self, x, y):
-        # Set position of the widget or window. May have no effect for widgets
-        glut.glutSetWindow(self._id)
-        glut.glutPositionWindow(x, y)
-
-    def _vispy_set_visible(self, visible):
-        # Show or hide the window or widget
-        glut.glutSetWindow(self._id)
-        if visible:
-            glut.glutShowWindow()
-        else:
-            glut.glutHideWindow()
-
-    def _vispy_update(self):
-        # Invoke a redraw
-        glut.glutSetWindow(self._id)
-        glut.glutPostRedisplay()
-
-    def _vispy_close(self):
-        # Force the window or widget to shut down
-        if self._closed:
-            return
-        self._vispy_canvas = None
-        # sometimes the context is already destroyed
-        try:
-            # prevent segfaults during garbage col
-            _set_close_fun(self._id, None)
-        except Exception:
-            pass
-        self._closed = True
-        self._vispy_set_visible(False)
-        # Try destroying the widget. Not in close event, because it isnt called
-        try:
-            glut.glutDestroyWindow(self._id)
-        except Exception:
-            pass
-
-    def _vispy_get_size(self):
-        glut.glutSetWindow(self._id)
-        w = glut.glutGet(glut.GLUT_WINDOW_WIDTH)
-        h = glut.glutGet(glut.GLUT_WINDOW_HEIGHT)
-        return w, h
-
-    def _vispy_get_position(self):
-        glut.glutSetWindow(self._id)
-        x = glut.glutGet(glut.GLUT_WINDOW_X)
-        y = glut.glutGet(glut.GLUT_WINDOW_Y)
-        return x, y
-
-    def _vispy_get_fullscreen(self):
-        return self._fullscreen
-
-    def _vispy_set_fullscreen(self, fullscreen):
-        old_val = self._fullscreen
-        self._fullscreen = bool(fullscreen)
-        if old_val != self._fullscreen:
-            if self._fullscreen:
-                self._old_size = self._vispy_get_size()
-                glut.glutFullScreen()
-            else:
-                self._vispy_set_size(*self._old_size)
-
-    def on_resize(self, w, h):
-        if self._vispy_canvas is None:
-            return
-        self._vispy_canvas.events.resize(size=(w, h))
-
-    def on_close(self):
-        if self._vispy_canvas is None:
-            return
-        self._vispy_canvas.close()
-
-    def on_draw(self, dummy=None):
-        if self._vispy_canvas is None:
-            return
-        #w = glut.glutGet(glut.GLUT_WINDOW_WIDTH)
-        #h = glut.glutGet(glut.GLUT_WINDOW_HEIGHT)
-        self._vispy_set_current()
-        self._vispy_canvas.events.draw(region=None)  # (0, 0, w, h))
-
-    def on_mouse_action(self, button, state, x, y):
-        if self._vispy_canvas is None:
-            return
-        action = {glut.GLUT_UP: 'release', glut.GLUT_DOWN: 'press'}[state]
-        mod = self._modifiers(False)
-
-        if button < 3:
-            # Mouse click event
-            button = BUTTONMAP.get(button, 0)
-            if action == 'press':
-                self._vispy_mouse_press(pos=(x, y), button=button,
-                                        modifiers=mod)
-            else:
-                self._vispy_mouse_release(pos=(x, y), button=button,
-                                          modifiers=mod)
-
-        elif button in (3, 4):
-            # Wheel event
-            deltay = 1.0 if button == 3 else -1.0
-            self._vispy_canvas.events.mouse_wheel(pos=(x, y),
-                                                  delta=(0.0, deltay),
-                                                  modifiers=mod)
-
-    def on_mouse_motion(self, x, y):
-        if self._vispy_canvas is None:
-            return
-        self._vispy_mouse_move(
-            pos=(x, y),
-            modifiers=self._modifiers(False),
-        )
-
-    def on_key_press(self, key, x, y):
-        key, text = self._process_key(key)
-        self._vispy_canvas.events.key_press(key=key, text=text,
-                                            modifiers=self._modifiers())
-
-    def on_key_release(self, key, x, y):
-        key, text = self._process_key(key)
-        self._vispy_canvas.events.key_release(key=key, text=text,
-                                              modifiers=self._modifiers())
-
-    def _process_key(self, key):
-        if key in KEYMAP:
-            if isinstance(key, int):
-                return KEYMAP[key], ''
-            else:
-                return KEYMAP[key], key
-        elif isinstance(key, int):
-            return None, ''  # unsupported special char
-        else:
-            return keys.Key(key.upper()), key
-
-    def _modifiers(self, query_now=True):
-        if query_now:
-            glutmod = glut.glutGetModifiers()
-            mod = ()
-            if glut.GLUT_ACTIVE_SHIFT & glutmod:
-                mod += keys.SHIFT,
-            if glut.GLUT_ACTIVE_CTRL & glutmod:
-                mod += keys.CONTROL,
-            if glut.GLUT_ACTIVE_ALT & glutmod:
-                mod += keys.ALT,
-            self._modifiers_cache = mod
-        return self._modifiers_cache
-
-
-# ------------------------------------------------------------------- timer ---
-
-# Note: we could also build a timer using glutTimerFunc, but this causes
-# trouble because timer callbacks appear to take precedence over all others.
-# Thus, a fast timer can block new display events.
-class TimerBackend(BaseTimerBackend):
-    def __init__(self, vispy_timer):
-        BaseTimerBackend.__init__(self, vispy_timer)
-        self._schedule = list()
-        glut.glutIdleFunc(self._idle_callback)
-        # tell application instance about existence
-        vispy_timer._app._backend._add_timer(self)
-
-    def _idle_callback(self):
-        now = ptime.time()
-        new_schedule = []
-
-        # see whether there are any timers ready
-        while len(self._schedule) > 0 and self._schedule[0][0] <= now:
-            timer = self._schedule.pop(0)[1]
-            timer._vispy_timer._timeout()
-            if timer._vispy_timer.running:
-                new_schedule.append((now + timer._vispy_timer.interval, timer))
-
-        # schedule next round of timeouts
-        if len(new_schedule) > 0:
-            self._schedule.extend(new_schedule)
-            self._schedule.sort()
-
-    def _vispy_start(self, interval):
-        now = ptime.time()
-        self._schedule.append((now + interval, self))
-
-    def _vispy_stop(self):
-        pass
-
-    def _vispy_get_native_timer(self):
-        return True  # glut has no native timer objects.
diff --git a/vispy/app/backends/_ipynb_static.py b/vispy/app/backends/_ipynb_static.py
index 213ea36..0b8802f 100644
--- a/vispy/app/backends/_ipynb_static.py
+++ b/vispy/app/backends/_ipynb_static.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -38,6 +38,7 @@ capability = dict(  # things that can be set by the backend
     multi_window=True,
     scroll=True,
     parent=False,
+    always_on_top=False,
 )
 
 
@@ -62,17 +63,11 @@ else:
     except Exception as exp:
         available, testable, why_not, which = False, False, str(exp), None
     else:
-        # Check if not GLUT, because that is going to be too unstable
-        if 'glut' in _app.backend_module.__name__:
-            _msg = 'ipynb_staatic backend refuses to work with GLUT'
-            available, testable, why_not, which = False, False, _msg, None
-        else:
-            available, testable, why_not = True, False, None
-            which = _app.backend_module.which
+        available, testable, why_not = True, False, None
+        which = _app.backend_module.which
 
     # Use that backend's shared context
     KEYMAP = _app.backend_module.KEYMAP
-    SharedContext = _app.backend_module.SharedContext
 
 
 # ------------------------------------------------------------- application ---
@@ -106,8 +101,10 @@ class ApplicationBackend(BaseApplicationBackend):
 
 class CanvasBackend(BaseCanvasBackend):
 
+    # args are for BaseCanvasBackend, kwargs are for us.
     def __init__(self, *args, **kwargs):
-        BaseCanvasBackend.__init__(self, capability, SharedContext)
+        BaseCanvasBackend.__init__(self, *args)
+        self._initialized = False
 
         # Test kwargs
 #         if kwargs['position']:
@@ -117,20 +114,19 @@ class CanvasBackend(BaseCanvasBackend):
         if kwargs['vsync']:
             raise RuntimeError('ipynb_static Canvas does not support vsync')
         if kwargs['fullscreen']:
-            raise RuntimeError('ipynb_static Canvas does not support \
-                               fullscreen')
+            raise RuntimeError('ipynb_static Canvas does not support '
+                               'fullscreen')
 
         # Create real canvas. It is a backend to this backend
         kwargs.pop('vispy_canvas', None)
         kwargs['autoswap'] = False
-        canvas = Canvas(app=_app, **kwargs)
+        canvas = Canvas(app=_app, **kwargs)  # Pass kwargs to underlying canvas
         self._backend2 = canvas.native
 
         # Connect to events of canvas to keep up to date with size and draw
         canvas.events.draw.connect(self._on_draw)
         canvas.events.resize.connect(self._on_resize)
-        self._initialized = False
-
+        
         # Show the widget
         canvas.show()
         # todo: hide that canvas
@@ -138,11 +134,6 @@ class CanvasBackend(BaseCanvasBackend):
         # Raw PNG that will be displayed on canvas.show()
         self._im = ""
 
-    @property
-    def _vispy_context(self):
-        """Context to return for sharing"""
-        return self._backend2._vispy_context
-
     def _vispy_warmup(self):
         return self._backend2._vispy_warmup()
 
@@ -202,7 +193,7 @@ class CanvasBackend(BaseCanvasBackend):
             self._vispy_canvas.events.initialize()
             self._on_resize()
         # Normal behavior
-        self._vispy_set_current()
+        self._vispy_canvas.set_current()
         self._vispy_canvas.events.draw(region=None)
 
         # Generate base64 encoded PNG string
diff --git a/vispy/app/backends/_ipynb_util.py b/vispy/app/backends/_ipynb_util.py
new file mode 100644
index 0000000..e7635ce
--- /dev/null
+++ b/vispy/app/backends/_ipynb_util.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""Tools used by the IPython notebook backends."""
+
+import re
+
+import numpy as np
+
+from ...ext.six import string_types, iteritems
+from ...util.logs import _serialize_buffer
+
+
+# -----------------------------------------------------------------------------
+# GLIR commands serialization
+# -----------------------------------------------------------------------------
+
+def _extract_buffers(commands):
+    """Extract all data buffers from the list of GLIR commands, and replace
+    them by buffer pointers {buffer: <buffer_index>}. Return the modified list
+    # of GILR commands and the list of buffers as well."""
+    # First, filter all DATA commands.
+    data_commands = [command for command in commands if command[0] == 'DATA']
+    # Extract the arrays.
+    buffers = [data_command[3] for data_command in data_commands]
+    # Modify the commands by replacing the array buffers with pointers.
+    commands_modified = list(commands)
+    buffer_index = 0
+    for i, command in enumerate(commands_modified):
+        if command[0] == 'DATA':
+            commands_modified[i] = command[:3] + \
+                ({'buffer_index': buffer_index},)
+            buffer_index += 1
+    return commands_modified, buffers
+
+
+def _serialize_item(item):
+    """Internal function: serialize native types."""
+    # Recursively serialize lists, tuples, and dicts.
+    if isinstance(item, (list, tuple)):
+        return [_serialize_item(subitem) for subitem in item]
+    elif isinstance(item, dict):
+        return dict([(key, _serialize_item(value))
+                     for (key, value) in iteritems(item)])
+
+    # Serialize strings.
+    elif isinstance(item, string_types):
+        # Replace glSomething by something (needed for WebGL commands).
+        if item.startswith('gl'):
+            return re.sub(r'^gl([A-Z])', lambda m: m.group(1).lower(), item)
+        else:
+            return item
+
+    # Process NumPy arrays that are not buffers (typically, uniform values).
+    elif isinstance(item, np.ndarray):
+        return _serialize_item(item.ravel().tolist())
+
+    # Serialize numbers.
+    else:
+        try:
+            return np.asscalar(item)
+        except Exception:
+            return item
+
+
+def _serialize_command(command_modified):
+    """Serialize a single GLIR (modified) command. The modification relates
+    to the fact that buffers are replaced by pointers."""
+    return _serialize_item(command_modified)
+
+
+def create_glir_message(commands, array_serialization=None):
+    """Create a JSON-serializable message of GLIR commands. NumPy arrays
+    are serialized according to the specified method.
+
+    Arguments
+    ---------
+
+    commands : list
+        List of GLIR commands.
+    array_serialization : string or None
+        Serialization method for NumPy arrays. Possible values are:
+            'binary' (default) : use a binary string
+            'base64' : base64 encoded string of the array
+
+    """
+    # Default serialization method for NumPy arrays.
+    if array_serialization is None:
+        array_serialization = 'binary'
+    # Extract the buffers.
+    commands_modified, buffers = _extract_buffers(commands)
+    # Serialize the modified commands (with buffer pointers) and the buffers.
+    commands_serialized = [_serialize_command(command_modified)
+                           for command_modified in commands_modified]
+    buffers_serialized = [_serialize_buffer(buffer, array_serialization)
+                          for buffer in buffers]
+    # Create the final message.
+    msg = {
+        'msg_type': 'glir_commands',
+        'commands': commands_serialized,
+        'buffers': buffers_serialized,
+    }
+    return msg
diff --git a/vispy/app/backends/_ipynb_vnc.py b/vispy/app/backends/_ipynb_vnc.py
index e541c06..65604c0 100644
--- a/vispy/app/backends/_ipynb_vnc.py
+++ b/vispy/app/backends/_ipynb_vnc.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -44,6 +44,7 @@ capability = dict(  # things that can be set by the backend
     multi_window=True,
     scroll=True,
     parent=False,
+    always_on_top=False,
 )
 
 
@@ -79,17 +80,11 @@ else:
     except Exception as exp:
         available, testable, why_not, which = False, False, str(exp), None
     else:
-        # Check if not GLUT, because that is going to be too unstable
-        if 'glut' in _app.backend_module.__name__:
-            _msg = 'ipynb_vnc backend refuses to work with GLUT'
-            available, testable, why_not, which = False, False, _msg, None
-        else:
-            available, testable, why_not = True, False, None
-            which = _app.backend_module.which
+        available, testable, why_not = True, False, None
+        which = _app.backend_module.which
         print('              NOTE: this backend requires the Chromium browser')
     # Use that backend's shared context
     KEYMAP = _app.backend_module.KEYMAP
-    SharedContext = _app.backend_module.SharedContext
 
 
 # ------------------------------------------------------------- application ---
@@ -123,8 +118,10 @@ class ApplicationBackend(BaseApplicationBackend):
 
 class CanvasBackend(BaseCanvasBackend):
 
+    # args are for BaseCanvasBackend, kwargs are for us.
     def __init__(self, *args, **kwargs):
-        BaseCanvasBackend.__init__(self, capability, SharedContext)
+        BaseCanvasBackend.__init__(self, *args)
+        self._initialized = False
 
         # Test kwargs
 #         if kwargs['size']:
@@ -141,13 +138,12 @@ class CanvasBackend(BaseCanvasBackend):
         # Create real canvas. It is a backend to this backend
         kwargs.pop('vispy_canvas', None)
         kwargs['autoswap'] = False
-        canvas = Canvas(app=_app, **kwargs)
+        canvas = Canvas(app=_app, **kwargs)  # Pass kwargs to underlying canvas
         self._backend2 = canvas.native
 
         # Connect to events of canvas to keep up to date with size and draws
         canvas.events.draw.connect(self._on_draw)
         canvas.events.resize.connect(self._on_resize)
-        self._initialized = False
 
         # Show the widget, we will hide it after the first time it's drawn
         self._backend2._vispy_set_visible(True)
@@ -158,11 +154,6 @@ class CanvasBackend(BaseCanvasBackend):
         # Create IPython Widget
         self._widget = Widget(self._gen_event, size=canvas.size)
 
-    @property
-    def _vispy_context(self):
-        """Context to return for sharing"""
-        return self._backend2._vispy_context
-
     def _vispy_warmup(self):
         return self._backend2._vispy_warmup()
 
@@ -237,7 +228,7 @@ class CanvasBackend(BaseCanvasBackend):
         self._backend2._vispy_set_visible(False)
 
         # Normal behavior
-        self._vispy_set_current()
+        self._vispy_canvas.set_current()
         self._vispy_canvas.events.draw(region=None)
         # Save the encoded screenshot image to widget
         self._save_screenshot()
diff --git a/vispy/app/backends/_ipynb_webgl.py b/vispy/app/backends/_ipynb_webgl.py
new file mode 100644
index 0000000..aedc7dc
--- /dev/null
+++ b/vispy/app/backends/_ipynb_webgl.py
@@ -0,0 +1,315 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+Vispy backend for the IPython notebook (WebGL approach).
+"""
+
+from __future__ import division
+
+from ..base import (BaseApplicationBackend, BaseCanvasBackend,
+                    BaseTimerBackend)
+from ...util import logger, keys
+from ...ext import six
+from vispy.gloo.glir import BaseGlirParser
+from vispy.app.backends.ipython import VispyWidget
+
+import os.path as op
+import os
+# -------------------------------------------------------------------- init ---
+
+capability = dict(  # things that can be set by the backend
+    title=True,  # But it only applies to the dummy window :P
+    size=True,  # We cannot possibly say we dont, because Canvas always sets it
+    position=True,  # Dito
+    show=True,
+    vsync=False,
+    resizable=True,
+    decorate=False,
+    fullscreen=True,
+    context=True,
+    multi_window=False,
+    scroll=True,
+    parent=False,
+    always_on_top=False,
+)
+
+# Try importing IPython
+try:
+    import tornado
+    import IPython
+    IPYTHON_MAJOR_VERSION = IPython.version_info[0]
+    if IPYTHON_MAJOR_VERSION < 2:
+        raise RuntimeError('ipynb_webgl backend requires IPython >= 2.0')
+    from IPython.html.nbextensions import install_nbextension
+    from IPython.display import display
+except Exception as exp:
+    # raise ImportError("The WebGL backend requires IPython >= 2.0")
+    available, testable, why_not, which = False, False, str(exp), None
+else:
+    available, testable, why_not, which = True, False, None, None
+
+
+# ------------------------------------------------------------- application ---
+def _prepare_js(force=False):
+    pkgdir = op.dirname(__file__)
+    jsdir = op.join(pkgdir, '../../html/static/js/')
+    # Make sure the JS files are installed to user directory (new argument
+    # in IPython 3.0).
+    if IPYTHON_MAJOR_VERSION >= 3:
+        kwargs = {'user': True}
+    else:
+        kwargs = {}
+    install_nbextension(jsdir, overwrite=force, destination='vispy',
+                        symlink=(os.name != 'nt'), **kwargs)
+
+
+class ApplicationBackend(BaseApplicationBackend):
+
+    def __init__(self):
+        BaseApplicationBackend.__init__(self)
+        _prepare_js()
+
+    def _vispy_reuse(self):
+        _prepare_js()
+
+    def _vispy_get_backend_name(self):
+        return 'ipynb_webgl'
+
+    def _vispy_process_events(self):
+        # TODO: may be implemented later.
+        raise NotImplementedError()
+
+    def _vispy_run(self):
+        pass
+
+    def _vispy_quit(self):
+        pass
+
+    def _vispy_get_native_app(self):
+        return self
+
+
+# ------------------------------------------------------------------ canvas ---
+class WebGLGlirParser(BaseGlirParser):
+    def __init__(self, widget=None):
+        self._widget = widget
+
+    def set_widget(self, widget):
+        self._widget = widget
+
+    def is_remote(self):
+        return True
+
+    def convert_shaders(self):
+        return 'es2'
+
+    def parse(self, commands):
+        self._widget.send_glir_commands(commands)
+
+
+class CanvasBackend(BaseCanvasBackend):
+    # args are for BaseCanvasBackend, kwargs are for us.
+    def __init__(self, *args, **kwargs):
+        BaseCanvasBackend.__init__(self, *args)
+        self._widget = None
+
+        p = self._process_backend_kwargs(kwargs)
+        self._context = p.context
+
+        # TODO: do something with context.config
+        # Take the context.
+        p.context.shared.add_ref('webgl', self)
+        if p.context.shared.ref is self:
+            pass  # ok
+        else:
+            raise RuntimeError("WebGL doesn't yet support context sharing.")
+
+        #store a default size before the widget is available.
+        #then we set the default size on the widget and only use the
+        #widget size
+        self._default_size = p.size
+        self._init_glir()
+
+    def set_widget(self, widget):
+        self._widget = widget
+        self._vispy_canvas.context.shared.parser.set_widget(widget)
+
+    def _init_glir(self):
+        context = self._vispy_canvas.context
+        context.shared.parser = WebGLGlirParser()
+
+    def _reinit_widget(self):
+        self._vispy_canvas.set_current()
+
+        self._vispy_canvas.events.initialize()
+        self._vispy_canvas.events.resize(size=(self._widget.width,
+                                               self._widget.height))
+        self._vispy_canvas.events.draw()
+
+    def _vispy_warmup(self):
+        pass
+
+    # Uncommenting these makes the backend crash.
+    def _vispy_set_current(self):
+        pass
+
+    def _vispy_swap_buffers(self):
+        pass
+
+    def _vispy_set_title(self, title):
+        raise NotImplementedError()
+
+    def _vispy_get_fullscreen(self):
+        # We don't want error messages to show up when the user presses
+        # F11 to fullscreen the browser.
+        pass
+
+    def _vispy_set_fullscreen(self, fullscreen):
+        # We don't want error messages to show up when the user presses
+        # F11 to fullscreen the browser.
+        pass
+
+    def _vispy_get_size(self):
+        if self._widget:
+            return (self._widget.width, self._widget.height)
+        else:
+            return self._default_size
+
+    def _vispy_set_size(self, w, h):
+        if self._widget:
+            self._widget.width = w
+            self._widget.height = h
+        else:
+            self._default_size = (w, h)
+
+    def _vispy_get_position(self):
+        raise NotImplementedError()
+
+    def _vispy_set_position(self, x, y):
+        logger.warning('IPython notebook canvas cannot be repositioned.')
+
+    def _vispy_set_visible(self, visible):
+        if not visible:
+            logger.warning('IPython notebook canvas cannot be hidden.')
+            return
+        if self._widget is None:
+            self._widget = VispyWidget()
+            self._widget.set_canvas(self._vispy_canvas)
+        display(self._widget)
+
+    def _vispy_update(self):
+        ioloop = tornado.ioloop.IOLoop.current()
+        ioloop.add_callback(self._draw_event)
+
+    def _draw_event(self):
+        self._vispy_canvas.set_current()
+        self._vispy_canvas.events.draw()
+
+    def _vispy_close(self):
+        raise NotImplementedError()
+
+    def _vispy_mouse_release(self, **kwargs):
+        # HACK: override this method from the base canvas in order to
+        # avoid breaking other backends.
+        kwargs.update(self._vispy_mouse_data)
+        ev = self._vispy_canvas.events.mouse_release(**kwargs)
+        if ev is None:
+            return
+        self._vispy_mouse_data['press_event'] = None
+        # TODO: this is a bit ugly, need to improve mouse button handling in
+        # app
+        ev._button = None
+        self._vispy_mouse_data['buttons'] = []
+        self._vispy_mouse_data['last_event'] = ev
+        return ev
+
+    # Generate vispy events according to upcoming JS events
+    _modifiers_map = {
+        'ctrl': keys.CONTROL,
+        'shift': keys.SHIFT,
+        'alt': keys.ALT,
+    }
+
+    def _gen_event(self, ev):
+        if self._vispy_canvas is None:
+            return
+        event_type = ev['type']
+        key_code = ev.get('key_code', None)
+        if key_code is None:
+            key, key_text = None, None
+        else:
+            if hasattr(keys, key_code):
+                key = getattr(keys, key_code)
+            else:
+                key = keys.Key(key_code)
+            # Generate the key text to pass to the event handler.
+            if key_code == 'SPACE':
+                key_text = ' '
+            else:
+                key_text = six.text_type(key_code)
+        # Process modifiers.
+        modifiers = ev.get('modifiers', None)
+        if modifiers:
+            modifiers = tuple([self._modifiers_map[modifier]
+                               for modifier in modifiers
+                               if modifier in self._modifiers_map])
+        if event_type == "mouse_move":
+            self._vispy_mouse_move(native=ev,
+                                   button=ev["button"],
+                                   pos=ev["pos"],
+                                   modifiers=modifiers,
+                                   )
+        elif event_type == "mouse_press":
+            self._vispy_mouse_press(native=ev,
+                                    pos=ev["pos"],
+                                    button=ev["button"],
+                                    modifiers=modifiers,
+                                    )
+        elif event_type == "mouse_release":
+            self._vispy_mouse_release(native=ev,
+                                      pos=ev["pos"],
+                                      button=ev["button"],
+                                      modifiers=modifiers,
+                                      )
+        elif event_type == "mouse_wheel":
+            self._vispy_canvas.events.mouse_wheel(native=ev,
+                                                  delta=ev["delta"],
+                                                  pos=ev["pos"],
+                                                  button=ev["button"],
+                                                  modifiers=modifiers,
+                                                  )
+        elif event_type == "key_press":
+            self._vispy_canvas.events.key_press(native=ev,
+                                                key=key,
+                                                text=key_text,
+                                                modifiers=modifiers,
+                                                )
+        elif event_type == "key_release":
+            self._vispy_canvas.events.key_release(native=ev,
+                                                  key=key,
+                                                  text=key_text,
+                                                  modifiers=modifiers,
+                                                  )
+        elif event_type == "resize":
+            self._vispy_canvas.events.resize(native=ev,
+                                             size=ev["size"])
+        elif event_type == "paint":
+            self._vispy_canvas.events.draw()
+
+
+# ------------------------------------------------------------------- Timer ---
+class TimerBackend(BaseTimerBackend):
+    def __init__(self, *args, **kwargs):
+        super(TimerBackend, self).__init__(*args, **kwargs)
+        self._timer = tornado.ioloop.PeriodicCallback(
+            self._vispy_timer._timeout,
+            1000)
+
+    def _vispy_start(self, interval):
+        self._timer.callback_time = interval * 1000
+        self._timer.start()
+
+    def _vispy_stop(self):
+        self._timer.stop()
diff --git a/vispy/app/backends/_pyglet.py b/vispy/app/backends/_pyglet.py
index 7942483..a2659dd 100644
--- a/vispy/app/backends/_pyglet.py
+++ b/vispy/app/backends/_pyglet.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -12,9 +12,12 @@ from distutils.version import LooseVersion
 from time import sleep
 
 from ..base import (BaseApplicationBackend, BaseCanvasBackend,
-                    BaseTimerBackend, BaseSharedContext)
+                    BaseTimerBackend)
 from ...util import keys
 from ...util.ptime import time
+from ... import config
+
+USE_EGL = config['gl_backend'].lower().startswith('es')
 
 
 # -------------------------------------------------------------------- init ---
@@ -83,7 +86,10 @@ except Exception as exp:
     class _Window(object):
         pass
 else:
-    available, testable, why_not = True, True, None
+    if USE_EGL:
+        available, testable, why_not = False, False, 'EGL not supported'
+    else:
+        available, testable, why_not = True, True, None
     which = 'pyglet ' + str(pyglet.version)
     _Window = pyglet.window.Window
 
@@ -103,6 +109,7 @@ capability = dict(  # things that can be set by the backend
     multi_window=True,
     scroll=True,
     parent=False,
+    always_on_top=False,
 )
 
 
@@ -130,10 +137,6 @@ def _set_config(config):
     return pyglet_config
 
 
-class SharedContext(BaseSharedContext):
-    _backend = 'pyglet'
-
-
 # ------------------------------------------------------------- application ---
 
 class ApplicationBackend(BaseApplicationBackend):
@@ -168,59 +171,52 @@ class CanvasBackend(_Window, BaseCanvasBackend):
 
     """ Pyglet backend for Canvas abstract class."""
 
-    def __init__(self, **kwargs):
-        BaseCanvasBackend.__init__(self, capability, SharedContext)
-        title, size, position, show, vsync, resize, dec, fs, parent, context, \
-            vispy_canvas = self._process_backend_kwargs(kwargs)
-        self._vispy_canvas = vispy_canvas
-        if not isinstance(context, (dict, SharedContext)):
-            raise TypeError('context must be a dict or pyglet SharedContext')
-        if not isinstance(context, SharedContext):
-            config = _set_config(context)  # transform to Pyglet config
-        else:
-            # contexts are shared by default in Pyglet, so we shouldn't need
-            # to do anything to share them...
-            config = None
-        style = (pyglet.window.Window.WINDOW_STYLE_DEFAULT if dec else
+    # args are for BaseCanvasBackend, kwargs are for us.
+    def __init__(self, *args, **kwargs):
+        BaseCanvasBackend.__init__(self, *args)
+        p = self._process_backend_kwargs(kwargs)
+
+        # Deal with config
+        config = _set_config(p.context.config)  # Also used further below
+        # Deal with context
+        p.context.shared.add_ref('pyglet', self)
+        # contexts are shared by default in Pyglet
+
+        style = (pyglet.window.Window.WINDOW_STYLE_DEFAULT if p.decorate else
                  pyglet.window.Window.WINDOW_STYLE_BORDERLESS)
         # We keep track of modifier keys so we can pass them to mouse_motion
         self._current_modifiers = set()
-        #self._buttons_accepted = 0
+        # self._buttons_accepted = 0
         self._draw_ok = False  # whether it is ok to draw yet
         self._pending_position = None
-        if fs is not False:
+        if p.fullscreen is not False:
             screen = pyglet.window.get_platform().get_default_display()
             self._vispy_fullscreen = True
-            if fs is True:
+            if p.fullscreen is True:
                 self._vispy_screen = screen.get_default_screen()
             else:
                 screen = screen.get_screens()
-                if fs >= len(screen):
+                if p.fullscreen >= len(screen):
                     raise RuntimeError('fullscreen must be < %s'
                                        % len(screen))
-                self._vispy_screen = screen[fs]
+                self._vispy_screen = screen[p.fullscreen]
         else:
             self._vispy_fullscreen = False
             self._vispy_screen = None
         self._initialize_sent = False
-        pyglet.window.Window.__init__(self, width=size[0], height=size[1],
-                                      caption=title, visible=show,
-                                      config=config, vsync=vsync,
-                                      resizable=resize, style=style,
+        pyglet.window.Window.__init__(self, width=p.size[0], height=p.size[1],
+                                      caption=p.title, visible=p.show,
+                                      config=config, vsync=p.vsync,
+                                      resizable=p.resizable, style=style,
                                       screen=self._vispy_screen)
-        if position is not None:
-            self._vispy_set_position(*position)
-
-    @property
-    def _vispy_context(self):
-        """Context to return for sharing"""
-        return SharedContext(None)
+        if p.position is not None:
+            self._vispy_set_position(*p.position)
 
     def _vispy_warmup(self):
         etime = time() + 0.1
         while time() < etime:
             sleep(0.01)
-            self._vispy_set_current()
+            self._vispy_canvas.set_current()
             self._vispy_canvas.app.process_events()
 
     # Override these ...
@@ -295,7 +291,7 @@ class CanvasBackend(_Window, BaseCanvasBackend):
             return
         if not self._initialize_sent:
             self._initialize_sent = True
-            self._vispy_set_current()
+            self._vispy_canvas.set_current()
             self._vispy_canvas.events.initialize()
         # Set location now if we must. For some reason we get weird
         # offsets in viewport if set_location is called before the
@@ -322,7 +318,7 @@ class CanvasBackend(_Window, BaseCanvasBackend):
         if not self._draw_ok or self._vispy_canvas is None:
             return
         # (0, 0, self.width, self.height))
-        self._vispy_set_current()
+        self._vispy_canvas.set_current()
         self._vispy_canvas.events.draw(region=None)
 
     def on_mouse_press(self, x, y, button, modifiers=None):
diff --git a/vispy/app/backends/_pyqt4.py b/vispy/app/backends/_pyqt4.py
index 46cebc8..cac7e2c 100644
--- a/vispy/app/backends/_pyqt4.py
+++ b/vispy/app/backends/_pyqt4.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ PyQt4 proxy backend for the qt backend. 
@@ -8,10 +8,21 @@
 import sys
 from .. import backends
 from ...util import logger
+from ... import config
+
+USE_EGL = config['gl_backend'].lower().startswith('es')
 
 try:
-    # Try importing
-    from PyQt4 import QtGui, QtCore, QtOpenGL  # noqa
+    # Make sure no conflicting libraries have been imported.
+    for lib in ['PySide', 'PyQt5']:
+        lib += '.QtCore'
+        if lib in sys.modules:
+            raise RuntimeError("Refusing to import PyQt4 because %s is "
+                               "already imported." % lib)
+    # Try importing (QtOpenGL first to fail without import QtCore)
+    if not USE_EGL:
+        from PyQt4 import QtOpenGL  # noqa
+    from PyQt4 import QtGui, QtCore  # noqa
 except Exception as exp:
     # Fail: this backend cannot be used
     available, testable, why_not, which = False, False, str(exp), None
diff --git a/vispy/app/backends/_pyqt5.py b/vispy/app/backends/_pyqt5.py
new file mode 100644
index 0000000..c0d4536
--- /dev/null
+++ b/vispy/app/backends/_pyqt5.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+""" PyQt5 proxy backend for the qt backend. 
+"""
+
+import sys
+from .. import backends
+from ...util import logger
+from ... import config
+
+USE_EGL = config['gl_backend'].lower().startswith('es')
+
+try:
+    # Make sure no conflicting libraries have been imported.
+    for lib in ['PySide', 'PyQt4']:
+        lib += '.QtCore'
+        if lib in sys.modules:
+            raise RuntimeError("Refusing to import PyQt5 because %s is "
+                               "already imported." % lib)
+    # Try importing (QtOpenGL first to fail without import QtCore)
+    if not USE_EGL:
+        from PyQt5 import QtOpenGL  # noqa
+    from PyQt5 import QtGui, QtCore  # noqa
+except Exception as exp:
+    # Fail: this backend cannot be used
+    available, testable, why_not, which = False, False, str(exp), None
+else:
+    # Success
+    available, testable, why_not = True, True, None
+    has_uic = True
+    which = ('PyQt5', QtCore.PYQT_VERSION_STR, QtCore.QT_VERSION_STR)
+    # Remove _qt module to force an import even if it was already imported
+    sys.modules.pop(__name__.replace('_pyqt5', '_qt'), None)
+    # Import _qt. Keep a ref to the module object!
+    if backends.qt_lib is None:
+        backends.qt_lib = 'pyqt5'  # Signal to _qt what it should import
+        from . import _qt  # noqa
+        from ._qt import *  # noqa
+    else:
+        logger.info('%s already imported, cannot switch to %s'
+                    % (backends.qt_lib, 'pyqt5'))
diff --git a/vispy/app/backends/_pyside.py b/vispy/app/backends/_pyside.py
index 762d96c..5a18769 100644
--- a/vispy/app/backends/_pyside.py
+++ b/vispy/app/backends/_pyside.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ PySide proxy backend for the qt backend. 
@@ -8,10 +8,21 @@
 import sys
 from .. import backends
 from ...util import logger
+from ... import config
+
+USE_EGL = config['gl_backend'].lower().startswith('es')
 
 try:
-    # Try importing
-    from PySide import QtGui, QtCore, QtOpenGL  # noqa
+    # Make sure no conflicting libraries have been imported.
+    for lib in ['PyQt4', 'PyQt5']:
+        lib += '.QtCore'
+        if lib in sys.modules:
+            raise RuntimeError("Refusing to import PySide because %s is "
+                               "already imported." % lib)
+    # Try importing (QtOpenGL first to fail without import QtCore)
+    if not USE_EGL:
+        from PySide import QtOpenGL  # noqa
+    from PySide import QtGui, QtCore  # noqa
 except Exception as exp:
     # Fail: this backend cannot be used
     available, testable, why_not, which = False, False, str(exp), None
diff --git a/vispy/app/backends/_qt.py b/vispy/app/backends/_qt.py
index c0e0f8d..39bc208 100644
--- a/vispy/app/backends/_qt.py
+++ b/vispy/app/backends/_qt.py
@@ -1,20 +1,20 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
-Base code for the PySide and PyQt4 backends. Note that this is *not*
-(anymore) a backend by itself! One has to explicitly use either PySide
-or PyQt4. Note that the automatic backend selection prefers a GUI
-toolkit that is already imported.
-
-The _pyside and _pyqt4 modules will import * from this module, and also
-keep a ref to the module object. Note that if both the PySide and PyQt4
-backend are used, this module is actually reloaded. This is a sorts of
-poor mans "subclassing" to get a working version for both backends using
-the same code.
-
-Note that it is strongly discouraged to use the PySide and PyQt4
+Base code for the Qt backends. Note that this is *not* (anymore) a
+backend by itself! One has to explicitly use either PySide, PyQt4 or
+PyQt5. Note that the automatic backend selection prefers a GUI toolkit
+that is already imported.
+
+The _pyside, _pyqt4 and _pyqt5 modules will import * from this module,
+and also keep a ref to the module object. Note that if two of the
+backends are used, this module is actually reloaded. This is a sorts
+of poor mans "subclassing" to get a working version for both backends
+using the same code.
+
+Note that it is strongly discouraged to use the PySide/PyQt4/PyQt5
 backends simultaneously. It is known to cause unpredictable behavior
 and segfaults.
 """
@@ -22,24 +22,67 @@ and segfaults.
 from __future__ import division
 
 from time import sleep, time
-from ...util import logger
+import os
+import sys
+import atexit
+import ctypes
 
+from ...util import logger
 from ..base import (BaseApplicationBackend, BaseCanvasBackend,
-                    BaseTimerBackend, BaseSharedContext)
+                    BaseTimerBackend)
 from ...util import keys
 from ...ext.six import text_type
-
+from ... import config
 from . import qt_lib
 
+USE_EGL = config['gl_backend'].lower().startswith('es')
+
+# Get platform
+IS_LINUX = IS_OSX = IS_WIN = IS_RPI = False
+if sys.platform.startswith('linux'):
+    if os.uname()[4].startswith('arm'):
+        IS_RPI = True
+    else:
+        IS_LINUX = True
+elif sys.platform.startswith('darwin'):
+    IS_OSX = True
+elif sys.platform.startswith('win'):
+    IS_WIN = True
 
 # -------------------------------------------------------------------- init ---
 
+
+def _check_imports(lib):
+    # Make sure no conflicting libraries have been imported.
+    libs = ['PyQt4', 'PyQt5', 'PySide']
+    libs.remove(lib)
+    for lib2 in libs:
+        lib2 += '.QtCore'
+        if lib2 in sys.modules:
+            raise RuntimeError("Refusing to import %s because %s is already "
+                               "imported." % (lib, lib2))
+
 # Get what qt lib to try. This tells us wheter this module is imported
-# via _pyside or _pyqt4
+# via _pyside or _pyqt4 or _pyqt5
+QGLWidget = object
 if qt_lib == 'pyqt4':
-    from PyQt4 import QtGui, QtCore, QtOpenGL
+    _check_imports('PyQt4')
+    if not USE_EGL:
+        from PyQt4.QtOpenGL import QGLWidget, QGLFormat
+    from PyQt4 import QtGui, QtCore
+    QWidget, QApplication = QtGui.QWidget, QtGui.QApplication  # Compat
+elif qt_lib == 'pyqt5':
+    _check_imports('PyQt5')
+    if not USE_EGL:
+        from PyQt5.QtOpenGL import QGLWidget, QGLFormat
+    from PyQt5 import QtGui, QtCore, QtWidgets
+    QWidget, QApplication = QtWidgets.QWidget, QtWidgets.QApplication  # Compat
 elif qt_lib == 'pyside':
-    from PySide import QtGui, QtCore, QtOpenGL
+    _check_imports('PySide')
+    if not USE_EGL:
+        from PySide.QtOpenGL import QGLWidget, QGLFormat
+    from PySide import QtGui, QtCore
+    QWidget, QApplication = QtGui.QWidget, QtGui.QApplication  # Compat
 elif qt_lib:
     raise RuntimeError("Invalid value for qt_lib %r." % qt_lib)
 else:
@@ -103,7 +146,11 @@ def message_handler(msg_type, msg):
     else:
         logger.warning(msg)
 
-QtCore.qInstallMsgHandler(message_handler)
+try:
+    QtCore.qInstallMsgHandler(message_handler)
+except AttributeError:
+    QtCore.qInstallMessageHandler(message_handler)  # PyQt5
+
 
 # -------------------------------------------------------------- capability ---
 
@@ -120,13 +167,14 @@ capability = dict(  # things that can be set by the backend
     multi_window=True,
     scroll=True,
     parent=True,
+    always_on_top=True,
 )
 
 
 # ------------------------------------------------------- set_configuration ---
 def _set_config(c):
     """Set the OpenGL configuration"""
-    glformat = QtOpenGL.QGLFormat()
+    glformat = QGLFormat()
     glformat.setRedBufferSize(c['red_size'])
     glformat.setGreenBufferSize(c['green_size'])
     glformat.setBlueBufferSize(c['blue_size'])
@@ -145,10 +193,6 @@ def _set_config(c):
     return glformat
 
 
-class SharedContext(BaseSharedContext):
-    _backend = 'qt'
-
-
 # ------------------------------------------------------------- application ---
 
 class ApplicationBackend(BaseApplicationBackend):
@@ -157,10 +201,8 @@ class ApplicationBackend(BaseApplicationBackend):
         BaseApplicationBackend.__init__(self)
 
     def _vispy_get_backend_name(self):
-        if 'pyside' in QtCore.__name__.lower():
-            return 'PySide (qt)'
-        else:
-            return 'PyQt4 (qt)'
+        name = QtCore.__name__.split('.')[0]
+        return name
 
     def _vispy_process_events(self):
         app = self._vispy_get_native_app()
@@ -179,11 +221,11 @@ class ApplicationBackend(BaseApplicationBackend):
 
     def _vispy_get_native_app(self):
         # Get native app in save way. Taken from guisupport.py
-        app = QtGui.QApplication.instance()
+        app = QApplication.instance()
         if app is None:
-            app = QtGui.QApplication([''])
+            app = QApplication([''])
         # Store so it won't be deleted, but not on a vispy object,
-        # or an application may produce error when closed
+        # or an application may produce error when closed.
         QtGui._qApp = app
         # Return
         return app
@@ -191,80 +233,49 @@ class ApplicationBackend(BaseApplicationBackend):
 
 # ------------------------------------------------------------------ canvas ---
 
-class CanvasBackend(QtOpenGL.QGLWidget, BaseCanvasBackend):
 
-    """Qt backend for Canvas abstract class."""
+class QtBaseCanvasBackend(BaseCanvasBackend):
+    """Base functionality of Qt backend. No OpenGL Stuff."""
 
+    # args are for BaseCanvasBackend, kwargs are for us.
     def __init__(self, *args, **kwargs):
+        BaseCanvasBackend.__init__(self, *args)
+        # Maybe to ensure that exactly all arguments are passed?
+        p = self._process_backend_kwargs(kwargs)
         self._initialized = False
-        BaseCanvasBackend.__init__(self, capability, SharedContext)
-        title, size, position, show, vsync, resize, dec, fs, parent, context, \
-            vispy_canvas = self._process_backend_kwargs(kwargs)
-        self._vispy_canvas = vispy_canvas
-        if isinstance(context, dict):
-            glformat = _set_config(context)
-            glformat.setSwapInterval(1 if vsync else 0)
-            widget = kwargs.pop('shareWidget', None)
-        else:
-            glformat = QtOpenGL.QGLFormat.defaultFormat()
-            if 'shareWidget' in kwargs:
-                raise RuntimeError('cannot use vispy to share context and '
-                                   'use built-in shareWidget')
-            widget = context.value
-        f = QtCore.Qt.Widget if dec else QtCore.Qt.FramelessWindowHint
 
-        # first arg can be glformat, or a shared context
-        QtOpenGL.QGLWidget.__init__(self, glformat, parent, widget, f)
-        self._initialized = True
-        if not self.isValid():
-            raise RuntimeError('context could not be created')
-        self.setAutoBufferSwap(False)  # to make consistent with other backends
+        # Init in desktop GL or EGL way
+        self._init_specific(p, kwargs)
+        assert self._initialized
+
         self.setMouseTracking(True)
-        self._vispy_set_title(title)
-        self._vispy_set_size(*size)
-        if fs is not False:
-            if fs is not True:
+        self._vispy_set_title(p.title)
+        self._vispy_set_size(*p.size)
+        if p.fullscreen is not False:
+            if p.fullscreen is not True:
                 logger.warning('Cannot specify monitor number for Qt '
                                'fullscreen, using default')
             self._fullscreen = True
         else:
             self._fullscreen = False
-        if not resize:
+        if not p.resizable:
             self.setFixedSize(self.size())
-        if position is not None:
-            self._vispy_set_position(*position)
-        self._init_show = show
-
-    def _vispy_init(self):
-        """Do actions that require self._vispy_canvas._backend to be set"""
-        if self._init_show:
+        if p.position is not None:
+            self._vispy_set_position(*p.position)
+        if p.show:
             self._vispy_set_visible(True)
 
-    @property
-    def _vispy_context(self):
-        """Context to return for sharing"""
-        return SharedContext(self)
+        # Qt supports OS double-click events, so we set this here to
+        # avoid double events
+        self._double_click_supported = True
 
     def _vispy_warmup(self):
         etime = time() + 0.25
         while time() < etime:
             sleep(0.01)
-            self._vispy_set_current()
+            self._vispy_canvas.set_current()
             self._vispy_canvas.app.process_events()
 
-    def _vispy_set_current(self):
-        # Make this the current context
-        if self._vispy_canvas is None:
-            return
-        if self.isValid():
-            self.makeCurrent()
-
-    def _vispy_swap_buffers(self):
-        # Swap front and back buffer
-        if self._vispy_canvas is None:
-            return
-        self.swapBuffers()
-
     def _vispy_set_title(self, title):
         # Set the window title. Has no effect for widgets
         if self._vispy_canvas is None:
@@ -302,12 +313,6 @@ class CanvasBackend(QtOpenGL.QGLWidget, BaseCanvasBackend):
         # Invoke a redraw
         self.update()
 
-    def _vispy_close(self):
-        # Force the window or widget to shut down
-        self.close()
-        self.doneCurrent()
-        self.context().reset()
-
     def _vispy_get_position(self):
         g = self.geometry()
         return g.x(), g.y()
@@ -316,28 +321,6 @@ class CanvasBackend(QtOpenGL.QGLWidget, BaseCanvasBackend):
         g = self.geometry()
         return g.width(), g.height()
 
-    def initializeGL(self):
-        if self._vispy_canvas is None:
-            return
-        self._vispy_canvas.events.initialize()
-
-    def resizeGL(self, w, h):
-        if self._vispy_canvas is None:
-            return
-        self._vispy_canvas.events.resize(size=(w, h))
-
-    def paintGL(self):
-        if self._vispy_canvas is None:
-            return
-        # (0, 0, self.width(), self.height()))
-        self._vispy_set_current()
-        self._vispy_canvas.events.draw(region=None)
-
-    def closeEvent(self, ev):
-        if self._vispy_canvas is None:
-            return
-        self._vispy_canvas.close()
-
     def sizeHint(self):
         return self.size()
 
@@ -348,7 +331,7 @@ class CanvasBackend(QtOpenGL.QGLWidget, BaseCanvasBackend):
             native=ev,
             pos=(ev.pos().x(), ev.pos().y()),
             button=BUTTONMAP.get(ev.button(), 0),
-            modifiers = self._modifiers(ev),
+            modifiers=self._modifiers(ev),
         )
 
     def mouseReleaseEvent(self, ev):
@@ -358,7 +341,17 @@ class CanvasBackend(QtOpenGL.QGLWidget, BaseCanvasBackend):
             native=ev,
             pos=(ev.pos().x(), ev.pos().y()),
             button=BUTTONMAP[ev.button()],
-            modifiers = self._modifiers(ev),
+            modifiers=self._modifiers(ev),
+        )
+
+    def mouseDoubleClickEvent(self, ev):
+        if self._vispy_canvas is None:
+            return
+        self._vispy_mouse_double_click(
+            native=ev,
+            pos=(ev.pos().x(), ev.pos().y()),
+            button=BUTTONMAP.get(ev.button(), 0),
+            modifiers=self._modifiers(ev),
         )
 
     def mouseMoveEvent(self, ev):
@@ -375,10 +368,15 @@ class CanvasBackend(QtOpenGL.QGLWidget, BaseCanvasBackend):
             return
         # Get scrolling
         deltax, deltay = 0.0, 0.0
-        if ev.orientation == QtCore.Qt.Horizontal:
-            deltax = ev.delta() / 120.0
+        if hasattr(ev, 'orientation'):
+            if ev.orientation == QtCore.Qt.Horizontal:
+                deltax = ev.delta() / 120.0
+            else:
+                deltay = ev.delta() / 120.0
         else:
-            deltay = ev.delta() / 120.0
+            # PyQt5
+            delta = ev.angleDelta()
+            deltax, deltay = delta.x() / 120.0, delta.y() / 120.0
         # Emit event
         self._vispy_canvas.events.mouse_wheel(
             native=ev,
@@ -418,14 +416,237 @@ class CanvasBackend(QtOpenGL.QGLWidget, BaseCanvasBackend):
         return mod
 
 
+_EGL_DISPLAY = None
+egl = None
+
+# todo: Make work on Windows
+# todo: Make work without readpixels on Linux?
+# todo: Make work on OSX?
+# todo: Make work on Raspberry Pi!
+
+
+class CanvasBackendEgl(QtBaseCanvasBackend, QWidget):
+
+    def _init_specific(self, p, kwargs):
+
+        # Initialize egl. Note that we only import egl if needed.
+        global _EGL_DISPLAY
+        global egl
+        if egl is None:
+            from ...ext import egl as _egl
+            egl = _egl
+            # Use MESA driver on Linux
+            if IS_LINUX and not IS_RPI:
+                os.environ['EGL_SOFTWARE'] = 'true'
+            # Create and init display
+            _EGL_DISPLAY = egl.eglGetDisplay()
+            CanvasBackendEgl._EGL_VERSION = egl.eglInitialize(_EGL_DISPLAY)
+            atexit.register(egl.eglTerminate, _EGL_DISPLAY)
+
+        # Deal with context
+        p.context.shared.add_ref('qt-egl', self)
+        if p.context.shared.ref is self:
+            self._native_config = c = egl.eglChooseConfig(_EGL_DISPLAY)[0]
+            self._native_context = egl.eglCreateContext(_EGL_DISPLAY, c, None)
+        else:
+            self._native_config = p.context.shared.ref._native_config
+            self._native_context = p.context.shared.ref._native_context
+
+        # Init widget
+        if p.always_on_top or not p.decorate:
+            hint = 0
+            hint |= 0 if p.decorate else QtCore.Qt.FramelessWindowHint
+            hint |= QtCore.Qt.WindowStaysOnTopHint if p.always_on_top else 0
+        else:
+            hint = QtCore.Qt.Widget  # can also be a window type
+        QWidget.__init__(self, p.parent, hint)
+
+        if 0:  # IS_LINUX or IS_RPI:
+            self.setAutoFillBackground(False)
+            self.setAttribute(QtCore.Qt.WA_NoSystemBackground, True)
+            self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, True)
+        elif IS_WIN:
+            self.setAttribute(QtCore.Qt.WA_PaintOnScreen, True)
+            self.setAutoFillBackground(False)
+
+        # Init surface
+        w = self.get_window_id()
+        self._surface = egl.eglCreateWindowSurface(_EGL_DISPLAY, c, w)
+        self.initializeGL()
+        self._initialized = True
+
+    def get_window_id(self):
+        """ Get the window id of a PySide Widget. Might also work for PyQt4.
+        """
+        # Get Qt win id
+        winid = self.winId()
+
+        # On Linux this is it
+        if IS_RPI:
+            nw = (ctypes.c_int * 3)(winid, self.width(), self.height())
+            return ctypes.pointer(nw)
+        elif IS_LINUX:
+            return int(winid)  # Is int on PySide, but sip.voidptr on PyQt
+
+        # Get window id from stupid capsule thingy
+        # http://translate.google.com/translate?hl=en&sl=zh-CN&u=http://www.cnb
+        #logs.com/Shiren-Y/archive/2011/04/06/2007288.html&prev=/search%3Fq%3Dp
+        # yside%2Bdirectx%26client%3Dfirefox-a%26hs%3DIsJ%26rls%3Dorg.mozilla:n
+        #l:official%26channel%3Dfflb%26biw%3D1366%26bih%3D614
+        # Prepare
+        ctypes.pythonapi.PyCapsule_GetName.restype = ctypes.c_char_p
+        ctypes.pythonapi.PyCapsule_GetName.argtypes = [ctypes.py_object]
+        ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
+        ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object,
+                                                          ctypes.c_char_p]
+        # Extract handle from capsule thingy
+        name = ctypes.pythonapi.PyCapsule_GetName(winid)
+        handle = ctypes.pythonapi.PyCapsule_GetPointer(winid, name)
+        return handle
+
+    def _vispy_close(self):
+        # Destroy EGL surface
+        if self._surface is not None:
+            egl.eglDestroySurface(_EGL_DISPLAY, self._surface)
+            self._surface = None
+        # Force the window or widget to shut down
+        self.close()
+
+    def _vispy_set_current(self):
+        egl.eglMakeCurrent(_EGL_DISPLAY, self._surface,
+                           self._surface, self._native_context)
+
+    def _vispy_swap_buffers(self):
+        egl.eglSwapBuffers(_EGL_DISPLAY, self._surface)
+
+    def initializeGL(self):
+        self._vispy_canvas.set_current()
+        self._vispy_canvas.events.initialize()
+
+    def resizeEvent(self, event):
+        w, h = event.size().width(), event.size().height()
+        self._vispy_canvas.events.resize(size=(w, h))
+
+    def paintEvent(self, event):
+        self._vispy_canvas.events.draw(region=None)
+
+        if IS_LINUX or IS_RPI:
+            # Arg, cannot get GL to draw to the widget, so we take a
+            # screenshot and draw that for now ...
+            # Further, QImage keeps a ref to the data that we pass, so
+            # we need to use a static buffer to prevent memory leakage
+            from vispy import gloo
+            import numpy as np
+            if not hasattr(self, '_gl_buffer'):
+                self._gl_buffer = np.ones((3000 * 3000 * 4), np.uint8) * 255
+            # Take screenshot and turn into RGB QImage
+            im = gloo.read_pixels()
+            sze = im.shape[0] * im.shape[1]
+            self._gl_buffer[0:0+sze*4:4] = im[:, :, 2].ravel()
+            self._gl_buffer[1:0+sze*4:4] = im[:, :, 1].ravel()
+            self._gl_buffer[2:2+sze*4:4] = im[:, :, 0].ravel()
+            img = QtGui.QImage(self._gl_buffer, im.shape[1], im.shape[0],
+                               QtGui.QImage.Format_RGB32)
+            # Paint the image
+            painter = QtGui.QPainter()
+            painter.begin(self)
+            rect = QtCore.QRect(0, 0, self.width(), self.height())
+            painter.drawImage(rect, img)
+            painter.end()
+
+    def paintEngine(self):
+        if IS_LINUX and not IS_RPI:
+            # For now we are drawing a screenshot
+            return QWidget.paintEngine(self)
+        else:
+            return None  # Disable Qt's native drawing system
+
+
+class CanvasBackendDesktop(QtBaseCanvasBackend, QGLWidget):
+
+    def _init_specific(self, p, kwargs):
+
+        # Deal with config
+        glformat = _set_config(p.context.config)
+        glformat.setSwapInterval(1 if p.vsync else 0)
+        # Deal with context
+        widget = kwargs.pop('shareWidget', None) or self
+        p.context.shared.add_ref('qt', widget)
+        if p.context.shared.ref is widget:
+            if widget is self:
+                widget = None  # QGLWidget does not accept self ;)
+        else:
+            widget = p.context.shared.ref
+            if 'shareWidget' in kwargs:
+                raise RuntimeError('Cannot use vispy to share context and '
+                                   'use built-in shareWidget.')
+
+        # first arg can be glformat, or a gl context
+        if p.always_on_top or not p.decorate:
+            hint = 0
+            hint |= 0 if p.decorate else QtCore.Qt.FramelessWindowHint
+            hint |= QtCore.Qt.WindowStaysOnTopHint if p.always_on_top else 0
+        else:
+            hint = QtCore.Qt.Widget  # can also be a window type
+        QGLWidget.__init__(self, glformat, p.parent, widget, hint)
+        self._initialized = True
+        if not self.isValid():
+            raise RuntimeError('context could not be created')
+        self.setAutoBufferSwap(False)  # to make consistent with other backends
+        self.setFocusPolicy(QtCore.Qt.WheelFocus)
+
+    def _vispy_close(self):
+        # Force the window or widget to shut down
+        self.close()
+        self.doneCurrent()
+        self.context().reset()
+
+    def _vispy_set_current(self):
+        if self._vispy_canvas is None:
+            return  # todo: can we get rid of this now?
+        if self.isValid():
+            self.makeCurrent()
+
+    def _vispy_swap_buffers(self):
+        # Swap front and back buffer
+        if self._vispy_canvas is None:
+            return
+        self.swapBuffers()
+
+    def initializeGL(self):
+        if self._vispy_canvas is None:
+            return
+        self._vispy_canvas.events.initialize()
+
+    def resizeGL(self, w, h):
+        if self._vispy_canvas is None:
+            return
+        self._vispy_canvas.events.resize(size=(w, h))
+
+    def paintGL(self):
+        if self._vispy_canvas is None:
+            return
+        # (0, 0, self.width(), self.height()))
+        self._vispy_canvas.set_current()
+        self._vispy_canvas.events.draw(region=None)
+
+
+# Select CanvasBackend
+if USE_EGL:
+    CanvasBackend = CanvasBackendEgl
+else:
+    CanvasBackend = CanvasBackendDesktop
+
+
 # ------------------------------------------------------------------- timer ---
 
 class TimerBackend(BaseTimerBackend, QtCore.QTimer):
 
     def __init__(self, vispy_timer):
-        if QtGui.QApplication.instance() is None:
-            global QAPP
-            QAPP = QtGui.QApplication([])
+        # Make sure there is an app
+        app = ApplicationBackend()
+        app._vispy_get_native_app()
+        # Init
         BaseTimerBackend.__init__(self, vispy_timer)
         QtCore.QTimer.__init__(self)
         self.timeout.connect(self._vispy_timeout)
diff --git a/vispy/app/backends/_sdl2.py b/vispy/app/backends/_sdl2.py
index fde660a..25213bd 100644
--- a/vispy/app/backends/_sdl2.py
+++ b/vispy/app/backends/_sdl2.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 vispy backend for sdl2.
@@ -14,9 +14,12 @@ import warnings
 import gc
 
 from ..base import (BaseApplicationBackend, BaseCanvasBackend,
-                    BaseTimerBackend, BaseSharedContext)
+                    BaseTimerBackend)
 from ...util import keys, logger
 from ...util.ptime import time
+from ... import config
+
+USE_EGL = config['gl_backend'].lower().startswith('es')
 
 
 # -------------------------------------------------------------------- init ---
@@ -78,7 +81,10 @@ try:
 except Exception as exp:
     available, testable, why_not, which = False, False, str(exp), None
 else:
-    available, testable, why_not = True, True, None
+    if USE_EGL:
+        available, testable, why_not = False, False, 'EGL not supported'
+    else:
+        available, testable, why_not = True, True, None
     which = 'sdl2 %d.%d.%d' % sdl2.version_info[:3]
 
 _SDL2_INITIALIZED = False
@@ -104,6 +110,7 @@ capability = dict(  # things that can be set by the backend
     multi_window=True,
     scroll=True,
     parent=False,
+    always_on_top=False,
 )
 
 
@@ -125,10 +132,6 @@ def _set_config(c):
     func(sdl2.SDL_GL_STEREO, c['stereo'])
 
 
-class SharedContext(BaseSharedContext):
-    _backend = 'sdl2'
-
-
 # ------------------------------------------------------------- application ---
 
 class ApplicationBackend(BaseApplicationBackend):
@@ -192,37 +195,44 @@ class CanvasBackend(BaseCanvasBackend):
 
     """ SDL2 backend for Canvas abstract class."""
 
-    def __init__(self, **kwargs):
-        BaseCanvasBackend.__init__(self, capability, SharedContext)
-        title, size, position, show, vsync, resize, dec, fs, parent, context, \
-            vispy_canvas = self._process_backend_kwargs(kwargs)
-        # Init SDL2, add window hints, and create window
-        if isinstance(context, dict):
-            _set_config(context)
+    # args are for BaseCanvasBackend, kwargs are for us.
+    def __init__(self, *args, **kwargs):
+        BaseCanvasBackend.__init__(self, *args)
+        p = self._process_backend_kwargs(kwargs)
+        self._initialized = False
+
+        # Deal with config
+        _set_config(p.context.config)
+        # Deal with context
+        p.context.shared.add_ref('sdl2', self)
+        if p.context.shared.ref is self:
             share = None
         else:
-            share = context.value
-            sdl2.SDL_GL_MakeCurrent(*share)  # old window must be current
+            other = p.context.shared.ref
+            share = other._id.window, other._native_context
+            sdl2.SDL_GL_MakeCurrent(*share)
             sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1)
 
-        sdl2.SDL_GL_SetSwapInterval(1 if vsync else 0)
+        sdl2.SDL_GL_SetSwapInterval(1 if p.vsync else 0)
         flags = sdl2.SDL_WINDOW_OPENGL
         flags |= sdl2.SDL_WINDOW_SHOWN  # start out shown
         flags |= sdl2.SDL_WINDOW_ALLOW_HIGHDPI
-        flags |= sdl2.SDL_WINDOW_RESIZABLE if resize else 0
-        flags |= sdl2.SDL_WINDOW_BORDERLESS if not dec else 0
-        if fs is not False:
+        flags |= sdl2.SDL_WINDOW_RESIZABLE if p.resizable else 0
+        flags |= sdl2.SDL_WINDOW_BORDERLESS if not p.decorate else 0
+        if p.fullscreen is not False:
             self._fullscreen = True
-            if fs is not True:
+            if p.fullscreen is not True:
                 logger.warning('Cannot specify monitor number for SDL2 '
                                'fullscreen, using default')
             flags |= sdl2.SDL_WINDOW_FULLSCREEN_DESKTOP
         else:
             self._fullscreen = False
         self._mods = list()
-        if position is None:
+        if p.position is None:
             position = [sdl2.SDL_WINDOWPOS_UNDEFINED] * 2
-        self._id = sdl2.ext.Window(title, size, position, flags)
+        else:
+            position = None
+        self._id = sdl2.ext.Window(p.title, p.size, position, flags)
         if not self._id.window:
             raise RuntimeError('Could not create window')
         if share is None:
@@ -231,40 +241,20 @@ class CanvasBackend(BaseCanvasBackend):
             self._native_context = sdl2.SDL_GL_CreateContext(share[0])
         self._sdl_id = sdl2.SDL_GetWindowID(self._id.window)
         _VP_SDL2_ALL_WINDOWS[self._sdl_id] = self
-        self._vispy_canvas_ = None
+
+        # Init
+        self._initialized = True
         self._needs_draw = False
-        self._vispy_set_current()
-        if not show:
+        self._vispy_canvas.set_current()
+        self._vispy_canvas.events.initialize()
+        if not p.show:
             self._vispy_set_visible(False)
-        self._initialized = False
-        self._vispy_canvas = vispy_canvas
-
-    @property
-    def _vispy_context(self):
-        """Context to return for sharing"""
-        return SharedContext((self._id.window, self._native_context))
-
-    ####################################
-    # Deal with events we get from vispy
-    @property
-    def _vispy_canvas(self):
-        """ The parent canvas/window """
-        return self._vispy_canvas_
-
-    @_vispy_canvas.setter
-    def _vispy_canvas(self, vc):
-        # Init events when the property is set by Canvas
-        self._vispy_canvas_ = vc
-        if vc is not None and not self._initialized:
-            self._initialized = True
-            self._vispy_set_current()
-            self._vispy_canvas.events.initialize()
 
     def _vispy_warmup(self):
         etime = time() + 0.1
         while time() < etime:
             sleep(0.01)
-            self._vispy_set_current()
+            self._vispy_canvas.set_current()
             self._vispy_canvas.app.process_events()
 
     def _vispy_set_current(self):
@@ -364,7 +354,7 @@ class CanvasBackend(BaseCanvasBackend):
     def _on_draw(self):
         if self._vispy_canvas is None or self._id is None:
             return
-        self._vispy_set_current()
+        self._vispy_canvas.set_current()
         self._vispy_canvas.events.draw(region=None)  # (0, 0, w, h))
 
     def _on_event(self, event):
diff --git a/vispy/app/backends/_template.py b/vispy/app/backends/_template.py
index 9e21074..4f82bfc 100644
--- a/vispy/app/backends/_template.py
+++ b/vispy/app/backends/_template.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ This module provides an template for creating backends for vispy.
@@ -10,8 +10,11 @@ should be emitted.
 from __future__ import division
 
 from ..base import (BaseApplicationBackend, BaseCanvasBackend,
-                    BaseTimerBackend, BaseSharedContext)
+                    BaseTimerBackend)
 from ...util import keys
+from ... import config
+
+USE_EGL = config['gl_backend'].lower().startswith('es')
 
 
 # -------------------------------------------------------------------- init ---
@@ -75,6 +78,7 @@ capability = dict(
     multi_window=False,   # can use multiple windows at once
     scroll=False,         # scroll-wheel events are supported
     parent=False,         # can pass native widget backend parent
+    always_on_top=False,  # can be made always-on-top
 )
 
 
@@ -84,10 +88,6 @@ def _set_config(c):
     raise NotImplementedError
 
 
-class SharedContext(BaseSharedContext):
-    _backend = 'template'
-
-
 # ------------------------------------------------------------- application ---
 
 class ApplicationBackend(BaseApplicationBackend):
@@ -128,6 +128,8 @@ class CanvasBackend(BaseCanvasBackend):
                                               modifiers=())
         self._vispy_canvas.events.mouse_release(pos=(x, y), button=1,
                                                 modifiers=())
+        self._vispy_canvas.events.mouse_double_click(pos=(x, y), button=1,
+                                                     modifiers=())
         self._vispy_canvas.events.mouse_move(pos=(x, y), modifiers=())
         self._vispy_canvas.events.mouse_wheel(pos=(x, y), delta=(0, 0),
                                               modifiers=())
@@ -149,9 +151,23 @@ class CanvasBackend(BaseCanvasBackend):
     events can be triggered.
     """
 
-    def __init__(self, vispy_canvas, *args, **kwargs):
-        # NativeWidgetClass.__init__(self, *args, **kwargs)
-        BaseCanvasBackend.__init__(self, vispy_canvas, SharedContext)
+    # args are for BaseCanvasBackend, kwargs are for us.
+    def __init__(self, *args, **kwargs):
+        BaseCanvasBackend.__init__(self, *args)
+        # We use _process_backend_kwargs() to "serialize" the kwargs
+        # and to check whether they match this backend's capability
+        p = self._process_backend_kwargs(kwargs)
+
+        # Deal with config
+        # ... use context.config
+        # Deal with context
+        p.context.shared.add_ref('backend-name', self)
+        if p.context.shared.ref is self:
+            self._native_context = None  # ...
+        else:
+            self._native_context = p.context.shared.ref._native_context
+
+        # NativeWidgetClass.__init__(self, foo, bar)
 
     def _vispy_set_current(self):
         # Make this the current context
diff --git a/vispy/app/backends/_test.py b/vispy/app/backends/_test.py
index 275d1c7..bd1bb41 100644
--- a/vispy/app/backends/_test.py
+++ b/vispy/app/backends/_test.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 available = False
diff --git a/vispy/app/backends/_wx.py b/vispy/app/backends/_wx.py
index d137c95..1e35c3d 100644
--- a/vispy/app/backends/_wx.py
+++ b/vispy/app/backends/_wx.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -13,9 +13,12 @@ import gc
 import warnings
 
 from ..base import (BaseApplicationBackend, BaseCanvasBackend,
-                    BaseTimerBackend, BaseSharedContext)
+                    BaseTimerBackend)
 from ...util import keys, logger
 from ...util.ptime import time
+from ... import config
+
+USE_EGL = config['gl_backend'].lower().startswith('es')
 
 
 # -------------------------------------------------------------------- init ---
@@ -24,7 +27,8 @@ try:
     # avoid silly locale warning on OSX
     with warnings.catch_warnings(record=True):
         import wx
-        from wx import Frame, glcanvas
+        from wx import glcanvas
+        from wx.glcanvas import GLCanvas
 
     # Map native keys to vispy keys
     KEYMAP = {
@@ -71,9 +75,11 @@ except Exception as exp:
 
     class GLCanvas(object):
         pass
-    Frame = GLCanvas
 else:
-    available, testable, why_not = True, True, None
+    if USE_EGL:
+        available, testable, why_not = False, False, 'EGL not supported'
+    else:
+        available, testable, why_not = True, True, None
     which = 'wxPython ' + str(wx.__version__)
 
 
@@ -92,6 +98,7 @@ capability = dict(  # things that can be set by the backend
     multi_window=True,
     scroll=True,
     parent=True,
+    always_on_top=True,
 )
 
 
@@ -111,10 +118,6 @@ def _set_config(c):
     return gl_attribs
 
 
-class SharedContext(BaseSharedContext):
-    _backend = 'pyglet'
-
-
 # ------------------------------------------------------------- application ---
 
 _wx_app = None
@@ -193,54 +196,71 @@ class DummySize(object):
         pass
 
 
-class CanvasBackend(Frame, BaseCanvasBackend):
+class CanvasBackend(GLCanvas, BaseCanvasBackend):
 
     """ wxPython backend for Canvas abstract class."""
 
-    def __init__(self, **kwargs):
-        BaseCanvasBackend.__init__(self, capability, SharedContext)
-        title, size, position, show, vsync, resize, dec, fs, parent, context, \
-            vispy_canvas = self._process_backend_kwargs(kwargs)
-        if not isinstance(context, (dict, SharedContext)):
-            raise TypeError('context must be a dict or wx SharedContext')
-        style = (wx.MINIMIZE_BOX | wx.MAXIMIZE_BOX | wx.CLOSE_BOX |
-                 wx.SYSTEM_MENU | wx.CAPTION | wx.CLIP_CHILDREN)
-        style |= wx.NO_BORDER if not dec else wx.RESIZE_BORDER
-        self._init = False
-        Frame.__init__(self, parent, wx.ID_ANY, title, position, size, style)
-        if not resize:
-            self.SetSizeHints(size[0], size[1], size[0], size[1])
-        if fs is not False:
-            if fs is not True:
-                logger.warning('Cannot specify monitor number for wx '
-                               'fullscreen, using default')
-            self._fullscreen = True
-        else:
-            self._fullscreen = False
-        _wx_app.SetTopWindow(self)
-        if not isinstance(context, SharedContext):
-            self._gl_attribs = _set_config(context)
+    # args are for BaseCanvasBackend, kwargs are for us.
+    def __init__(self, *args, **kwargs):
+        BaseCanvasBackend.__init__(self, *args)
+        p = self._process_backend_kwargs(kwargs)
+
+        # WX supports OS double-click events, so we set this here to
+        # avoid double events
+        self._double_click_supported = True
+
+        # Set config
+        self._gl_attribs = _set_config(p.context.config)
+        # Deal with context
+        p.context.shared.add_ref('wx', self)
+        if p.context.shared.ref is self:
+            self._gl_context = None  # set for real once we init the GLCanvas
         else:
-            self._gl_attribs = context.value[0]
-        self._canvas = glcanvas.GLCanvas(self, wx.ID_ANY, wx.DefaultPosition,
-                                         wx.DefaultSize, 0, 'GLCanvas',
-                                         self._gl_attribs)
-        self._canvas.Raise()
-        self._canvas.SetFocus()
-        self._vispy_set_title(title)
-        if not isinstance(context, SharedContext):
-            self._context = glcanvas.GLContext(self._canvas)
+            self._gl_context = p.context.shared.ref._gl_context
+
+        if p.parent is None:
+            style = (wx.MINIMIZE_BOX | wx.MAXIMIZE_BOX | wx.CLOSE_BOX |
+                     wx.SYSTEM_MENU | wx.CAPTION | wx.CLIP_CHILDREN)
+            style |= wx.NO_BORDER if not p.decorate else wx.RESIZE_BORDER
+            style |= wx.STAY_ON_TOP if p.always_on_top else 0
+            self._frame = wx.Frame(None, wx.ID_ANY, p.title, p.position,
+                                   p.size, style)
+            if not p.resizable:
+                self._frame.SetSizeHints(p.size[0], p.size[1],
+                                         p.size[0], p.size[1])
+            if p.fullscreen is not False:
+                if p.fullscreen is not True:
+                    logger.warning('Cannot specify monitor number for wx '
+                                   'fullscreen, using default')
+                self._fullscreen = True
+            else:
+                self._fullscreen = False
+            _wx_app.SetTopWindow(self._frame)
+            parent = self._frame
+            self._frame.Raise()
+            self._frame.Bind(wx.EVT_CLOSE, self.on_close)
         else:
-            self._context = context.value[1]
+            parent = p.parent
+            self._frame = None
+            self._fullscreen = False
+        self._init = False
+        GLCanvas.__init__(self, parent, wx.ID_ANY, pos=p.position,
+                          size=p.size, style=0, name='GLCanvas',
+                          attribList=self._gl_attribs)
+
+        if self._gl_context is None:
+            self._gl_context = glcanvas.GLContext(self)
+
+        self.SetFocus()
+        self._vispy_set_title(p.title)
         self._size = None
         self.Bind(wx.EVT_SIZE, self.on_resize)
-        self._canvas.Bind(wx.EVT_PAINT, self.on_paint)
-        self._canvas.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
-        self._canvas.Bind(wx.EVT_KEY_UP, self.on_key_up)
-        self._canvas.Bind(wx.EVT_MOUSE_EVENTS, self.on_mouse_event)
-        self.Bind(wx.EVT_CLOSE, self.on_close)
-        self._size_init = size
-        self._vispy_set_visible(show)
+        self.Bind(wx.EVT_PAINT, self.on_draw)
+        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
+        self.Bind(wx.EVT_KEY_UP, self.on_key_up)
+        self.Bind(wx.EVT_MOUSE_EVENTS, self.on_mouse_event)
+        self._size_init = p.size
+        self._vispy_set_visible(p.show)
 
     def on_resize(self, event):
         if self._vispy_canvas is None or not self._init:
@@ -251,13 +271,13 @@ class CanvasBackend(Frame, BaseCanvasBackend):
         self.Refresh()
         event.Skip()
 
-    def on_paint(self, event):
-        if self._vispy_canvas is None or self._canvas is None:
+    def on_draw(self, event):
+        if self._vispy_canvas is None:
             return
         dc = wx.PaintDC(self)  # needed for wx
         if not self._init:
             self._initialize()
-        self._vispy_set_current()
+        self._vispy_canvas.set_current()
         self._vispy_canvas.events.draw(region=None)
         del dc
         event.Skip()
@@ -266,37 +286,29 @@ class CanvasBackend(Frame, BaseCanvasBackend):
         if self._vispy_canvas is None:
             return
         self._init = True
-        self._vispy_set_current()
+        self._vispy_canvas.set_current()
         self._vispy_canvas.events.initialize()
         self.on_resize(DummySize(self._size_init))
 
     def _vispy_set_current(self):
-        if self._canvas is None:
-            return
-        self._canvas.SetCurrent(self._context)
-
-    @property
-    def _vispy_context(self):
-        """Context to return for sharing"""
-        return SharedContext([self._gl_attribs, self._context])
+        self.SetCurrent(self._gl_context)
 
     def _vispy_warmup(self):
         etime = time() + 0.3
         while time() < etime:
             sleep(0.01)
-            self._vispy_set_current()
+            self._vispy_canvas.set_current()
             self._vispy_canvas.app.process_events()
 
     def _vispy_swap_buffers(self):
         # Swap front and back buffer
-        if self._canvas is None:
-            return
-        self._vispy_set_current()
-        self._canvas.SwapBuffers()
+        self._vispy_canvas.set_current()
+        self.SwapBuffers()
 
     def _vispy_set_title(self, title):
         # Set the window title. Has no effect for widgets
-        self.SetLabel(title)
+        if self._frame is not None:
+            self._frame.SetLabel(title)
 
     def _vispy_set_size(self, w, h):
         # Set size of the widget or window
@@ -306,57 +318,63 @@ class CanvasBackend(Frame, BaseCanvasBackend):
 
     def _vispy_set_position(self, x, y):
         # Set positionof the widget or window. May have no effect for widgets
-        self.SetPosition((x, y))
+        if self._frame is not None:
+            self._frame.SetPosition((x, y))
 
     def _vispy_get_fullscreen(self):
         return self._fullscreen
 
     def _vispy_set_fullscreen(self, fullscreen):
-        self._fullscreen = bool(fullscreen)
-        self._vispy_set_visible(True)
+        if self._frame is not None:
+            self._fullscreen = bool(fullscreen)
+            self._vispy_set_visible(True)
 
     def _vispy_set_visible(self, visible):
         # Show or hide the window or widget
         self.Show(visible)
         if visible:
-            self.ShowFullScreen(self._fullscreen)
+            if self._frame is not None:
+                self._frame.ShowFullScreen(self._fullscreen)
 
     def _vispy_update(self):
         # Invoke a redraw
         self.Refresh()
 
     def _vispy_close(self):
-        if self._vispy_canvas is None or self._canvas is None:
+        if self._vispy_canvas is None:
             return
         # Force the window or widget to shut down
-        canvas = self._canvas
-        self._canvas = None
-        self._context = None  # let RC destroy this in case it's shared
+        canvas = self
+        frame = self._frame
+        self._gl_context = None  # let RC destroy this in case it's shared
         canvas.Close()
         canvas.Destroy()
-        self.Close()
-        self.Destroy()
+        if frame:
+            frame.Close()
+            frame.Destroy()
         gc.collect()  # ensure context gets destroyed if it should be
 
     def _vispy_get_size(self):
-        if self._canvas is None or self._vispy_canvas is None:
+        if self._vispy_canvas is None:
             return
         w, h = self.GetClientSize()
         return w, h
 
     def _vispy_get_position(self):
-        if self._vispy_canvas is None or self._canvas is None:
+        if self._vispy_canvas is None:
             return
         x, y = self.GetPosition()
         return x, y
 
     def on_close(self, evt):
+        if not self:  # wx control evaluates to false if C++ part deleted
+            return
         if self._vispy_canvas is None:
             return
         self._vispy_canvas.close()
 
     def on_mouse_event(self, evt):
-        if self._vispy_canvas is None or self._canvas is None:
+        if self._vispy_canvas is None:
             return
         pos = (evt.GetX(), evt.GetY())
         mods = _get_mods(evt)
@@ -386,17 +404,29 @@ class CanvasBackend(Frame, BaseCanvasBackend):
             else:
                 evt.Skip()
             self._vispy_mouse_release(pos=pos, button=button, modifiers=mods)
+        elif evt.ButtonDClick():
+            if evt.LeftDClick():
+                button = 0
+            elif evt.MiddleDClick():
+                button = 1
+            elif evt.RightDClick():
+                button = 2
+            else:
+                evt.Skip()
+            self._vispy_mouse_press(pos=pos, button=button, modifiers=mods)
+            self._vispy_mouse_double_click(pos=pos, button=button,
+                                           modifiers=mods)
         evt.Skip()
 
     def on_key_down(self, evt):
-        if self._vispy_canvas is None or self._canvas is None:
+        if self._vispy_canvas is None:
             return
         key, text = _process_key(evt)
         self._vispy_canvas.events.key_press(key=key, text=text,
                                             modifiers=_get_mods(evt))
 
     def on_key_up(self, evt):
-        if self._vispy_canvas is None or self._canvas is None:
+        if self._vispy_canvas is None:
             return
         key, text = _process_key(evt)
         self._vispy_canvas.events.key_release(key=key, text=text,
diff --git a/vispy/scene/visuals/line/__init__.py b/vispy/app/backends/ipython/__init__.py
old mode 100644
new mode 100755
similarity index 51%
copy from vispy/scene/visuals/line/__init__.py
copy to vispy/app/backends/ipython/__init__.py
index b4cef19..e750649
--- a/vispy/scene/visuals/line/__init__.py
+++ b/vispy/app/backends/ipython/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2014, 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
-from .line import Line  # noqa
+from ._widget import VispyWidget  # NOQA
diff --git a/vispy/app/backends/ipython/_widget.py b/vispy/app/backends/ipython/_widget.py
new file mode 100644
index 0000000..fab2f2c
--- /dev/null
+++ b/vispy/app/backends/ipython/_widget.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+try:
+    from IPython.html.widgets import DOMWidget
+    from IPython.utils.traitlets import Unicode, Int, Bool
+except Exception as exp:
+    # Init dummy objects needed to import this module withour errors.
+    # These are all overwritten with imports from IPython (on success)
+    DOMWidget = object
+    Unicode = Int = Float = Bool = lambda *args, **kwargs: None
+    available, testable, why_not, which = False, False, str(exp), None
+else:
+    available, testable, why_not, which = True, False, None, None
+from vispy.app.backends._ipynb_util import create_glir_message
+from vispy.app import Timer
+
+
+# ---------------------------------------------------------- IPython Widget ---
+def _stop_timers(canvas):
+    """Stop all timers in a canvas."""
+    for attr in dir(canvas):
+        try:
+            attr_obj = getattr(canvas, attr)
+        except NotImplementedError:
+            # This try/except is needed because canvas.position raises
+            # an error (it is not implemented in this backend).
+            attr_obj = None
+        if isinstance(attr_obj, Timer):
+            attr_obj.stop()
+
+
+class VispyWidget(DOMWidget):
+    _view_name = Unicode("VispyView", sync=True)
+    _view_module = Unicode('/nbextensions/vispy/webgl-backend.js', sync=True)
+
+    #height/width of the widget is managed by IPython.
+    #it's a string and can be anything valid in CSS.
+    #here we only manage the size of the viewport.
+    width = Int(sync=True)
+    height = Int(sync=True)
+    resizable = Bool(value=True, sync=True)
+
+    def __init__(self, **kwargs):
+        super(VispyWidget, self).__init__(**kwargs)
+        self.on_msg(self.events_received)
+        self.canvas = None
+        self.canvas_backend = None
+        self.gen_event = None
+
+    def set_canvas(self, canvas):
+        self.width, self.height = canvas._backend._default_size
+        self.canvas = canvas
+        self.canvas_backend = self.canvas._backend
+        self.canvas_backend.set_widget(self)
+        self.gen_event = self.canvas_backend._gen_event
+        #setup the backend widget then.
+
+    def events_received(self, _, msg):
+        if msg['msg_type'] == 'init':
+            self.canvas_backend._reinit_widget()
+        elif msg['msg_type'] == 'events':
+            events = msg['contents']
+            for ev in events:
+                self.gen_event(ev)
+        elif msg['msg_type'] == 'status':
+            if msg['contents'] == 'removed':
+                # Stop all timers associated to the widget.
+                _stop_timers(self.canvas_backend._vispy_canvas)
+
+    def send_glir_commands(self, commands):
+        # TODO: check whether binary websocket is available (ipython >= 3)
+        # Until IPython 3.0 is released, use base64.
+        array_serialization = 'base64'
+        # array_serialization = 'binary'
+        if array_serialization == 'base64':
+            msg = create_glir_message(commands, 'base64')
+            msg['array_serialization'] = 'base64'
+            self.send(msg)
+        elif array_serialization == 'binary':
+            msg = create_glir_message(commands, 'binary')
+            msg['array_serialization'] = 'binary'
+            # Remove the buffers from the JSON message: they will be sent
+            # independently via binary WebSocket.
+            buffers = msg.pop('buffers')
+            self.comm.send({"method": "custom", "content": msg},
+                           buffers=buffers)
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/app/backends/tests/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/app/backends/tests/__init__.py
diff --git a/vispy/app/backends/tests/test_ipynb_util.py b/vispy/app/backends/tests/test_ipynb_util.py
new file mode 100644
index 0000000..4566255
--- /dev/null
+++ b/vispy/app/backends/tests/test_ipynb_util.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+import numpy as np
+
+from vispy.app.backends._ipynb_util import (_extract_buffers,
+                                            _serialize_command,
+                                            create_glir_message)
+from vispy.testing import run_tests_if_main, assert_equal
+
+
+def test_extract_buffers():
+    arr = np.random.rand(10, 2).astype(np.float32)
+    arr2 = np.random.rand(20, 2).astype(np.int16)
+
+    # No DATA command.
+    commands = [('CREATE', 4, 'VertexBuffer')]
+    commands_modified, buffers = _extract_buffers(commands)
+    assert_equal(commands_modified, commands)
+    assert_equal(buffers, [])
+
+    # A single DATA command.
+    commands = [('DATA', 4, 0, arr)]
+    commands_modified, buffers = _extract_buffers(commands)
+    assert_equal(commands_modified, [('DATA', 4, 0, {'buffer_index': 0})])
+    assert_equal(buffers, [arr])
+
+    # Several commands.
+    commands = [('DATA', 0, 10, arr),
+                ('UNIFORM', 4, 'u_scale', 'vec3', (1, 2, 3)),
+                ('DATA', 2, 20, arr2)
+                ]
+    commands_modified_expected = [
+        ('DATA', 0, 10, {'buffer_index': 0}),
+        ('UNIFORM', 4, 'u_scale', 'vec3', (1, 2, 3)),
+        ('DATA', 2, 20, {'buffer_index': 1})]
+    commands_modified, buffers = _extract_buffers(commands)
+    assert_equal(commands_modified, commands_modified_expected)
+    assert_equal(buffers, [arr, arr2])
+
+
+def test_serialize_command():
+    command = ('CREATE', 4, 'VertexBuffer')
+    command_serialized = _serialize_command(command)
+    assert_equal(command_serialized, list(command))
+
+    command = ('UNIFORM', 4, 'u_scale', 'vec3', (1, 2, 3))
+    commands_serialized_expected = ['UNIFORM', 4, 'u_scale', 'vec3', [1, 2, 3]]
+    command_serialized = _serialize_command(command)
+    assert_equal(command_serialized, commands_serialized_expected)
+
+
+def test_create_glir_message_binary():
+    arr = np.zeros((3, 2)).astype(np.float32)
+    arr2 = np.ones((4, 5)).astype(np.int16)
+
+    commands = [('CREATE', 1, 'VertexBuffer'),
+                ('UNIFORM', 2, 'u_scale', 'vec3', (1, 2, 3)),
+                ('DATA', 3, 0, arr),
+                ('UNIFORM', 4, 'u_pan', 'vec2', np.array([1, 2, 3])),
+                ('DATA', 5, 20, arr2)]
+    msg = create_glir_message(commands)
+    assert_equal(msg['msg_type'], 'glir_commands')
+
+    commands_serialized = msg['commands']
+    assert_equal(commands_serialized,
+                 [['CREATE', 1, 'VertexBuffer'],
+                  ['UNIFORM', 2, 'u_scale', 'vec3', [1, 2, 3]],
+                  ['DATA', 3, 0, {'buffer_index': 0}],
+                  ['UNIFORM', 4, 'u_pan', 'vec2', [1, 2, 3]],
+                  ['DATA', 5, 20, {'buffer_index': 1}]])
+
+    buffers_serialized = msg['buffers']
+    buf0 = buffers_serialized[0]
+    assert_equal(buf0, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')  # noqa
+
+    buf1 = buffers_serialized[1]
+    assert_equal(buf1, b'\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00')  # noqa
+
+
+def test_create_glir_message_base64():
+    arr = np.zeros((3, 2)).astype(np.float32)
+    arr2 = np.ones((4, 5)).astype(np.int16)
+
+    commands = [('CREATE', 1, 'VertexBuffer'),
+                ('UNIFORM', 2, 'u_scale', 'vec3', (1, 2, 3)),
+                ('DATA', 3, 0, arr),
+                ('UNIFORM', 4, 'u_pan', 'vec2', np.array([1, 2, 3])),
+                ('DATA', 5, 20, arr2)]
+    msg = create_glir_message(commands, array_serialization='base64')
+    assert_equal(msg['msg_type'], 'glir_commands')
+
+    commands_serialized = msg['commands']
+    assert_equal(commands_serialized,
+                 [['CREATE', 1, 'VertexBuffer'],
+                  ['UNIFORM', 2, 'u_scale', 'vec3', [1, 2, 3]],
+                  ['DATA', 3, 0, {'buffer_index': 0}],
+                  ['UNIFORM', 4, 'u_pan', 'vec2', [1, 2, 3]],
+                  ['DATA', 5, 20, {'buffer_index': 1}]])
+
+    buffers_serialized = msg['buffers']
+    buf0 = buffers_serialized[0]
+    assert_equal(buf0['storage_type'], 'base64')
+    assert_equal(buf0['buffer'], 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
+
+    buf1 = buffers_serialized[1]
+    assert_equal(buf0['storage_type'], 'base64')
+    assert_equal(buf1['buffer'],
+                 'AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAA==')
+
+
+run_tests_if_main()
diff --git a/vispy/app/base.py b/vispy/app/base.py
index 2fe6d5b..7ed6b02 100644
--- a/vispy/app/base.py
+++ b/vispy/app/base.py
@@ -1,9 +1,8 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
-from inspect import getargspec
-from copy import deepcopy
-
-from ._config import get_default_config
+from ..util import SimpleBunch
 
 
 class BaseApplicationBackend(object):
@@ -23,6 +22,10 @@ class BaseApplicationBackend(object):
     def _vispy_run(self):
         raise NotImplementedError()
 
+    def _vispy_reuse(self):
+        # Does nothing by default.
+        pass
+
     def _vispy_quit(self):
         raise NotImplementedError()
 
@@ -45,74 +48,52 @@ class BaseCanvasBackend(object):
     the canvas itself.
     """
 
-    def __init__(self, capability, context_type):
-        # Initially the backend starts out with no canvas.
-        # Canvas takes care of setting this for us.
-        self._vispy_canvas = None
+    def __init__(self, vispy_canvas):
+        from .canvas import Canvas  # Avoid circular import
+        assert isinstance(vispy_canvas, Canvas)
+        self._vispy_canvas = vispy_canvas
+
+        # We set the _backend attribute of the vispy_canvas to self,
+        # because at the end of the __init__ of the CanvasBackend
+        # implementation there might be a call to show or draw. By
+        # setting it here, we ensure that the Canvas is "ready to go".
+        vispy_canvas._backend = self
 
         # Data used in the construction of new mouse events
         self._vispy_mouse_data = {
             'buttons': [],
             'press_event': None,
             'last_event': None,
+            'last_mouse_press': None,
         }
-        self._vispy_capability = capability
-        self._vispy_context_type = context_type
 
     def _process_backend_kwargs(self, kwargs):
-        """Removes vispy-specific kwargs for CanvasBackend"""
-        # these are the output arguments
+        """ Simple utility to retrieve kwargs in predetermined order.
+        Also checks whether the values of the backend arguments do not
+        violate the backend capabilities.
+        """
+        # Verify given argument with capability of the backend
+        app = self._vispy_canvas.app
+        capability = app.backend_module.capability
+        if kwargs['context'].shared.name:  # name already assigned: shared
+            if not capability['context']:
+                raise RuntimeError('Cannot share context with this backend')
+        for key in [key for (key, val) in capability.items() if not val]:
+            if key in ['context', 'multi_window', 'scroll']:
+                continue
+            invert = key in ['resizable', 'decorate']
+            if bool(kwargs[key]) - invert:
+                raise RuntimeError('Config %s is not supported by backend %s'
+                                   % (key, app.backend_name))
+
+        # Return items in sequence
+        out = SimpleBunch()
         keys = ['title', 'size', 'position', 'show', 'vsync', 'resizable',
-                'decorate', 'fullscreen', 'parent']
-        from .canvas import Canvas
-        outs = []
-        spec = getargspec(Canvas.__init__)
+                'decorate', 'fullscreen', 'parent', 'context', 'always_on_top',
+                ]
         for key in keys:
-            default = spec.defaults[spec.args.index(key) - 1]
-            out = kwargs.get(key, default)
-            if out != default and self._vispy_capability[key] is False:
-                raise RuntimeError('Cannot set property %s using this '
-                                   'backend' % key)
-            outs.append(out)
-
-        # now we add context, which we have to treat slightly differently
-        default_config = get_default_config()
-        context = kwargs.get('context', default_config)
-        can_share = self._vispy_capability['context']
-        # check the type
-        if isinstance(context, self._vispy_context_type):
-            if not can_share:
-                raise RuntimeError('Cannot share context with this backend')
-        elif isinstance(context, dict):
-            # first, fill in context with any missing entries
-            context = deepcopy(context)
-            for key, val in default_config.items():
-                context[key] = context.get(key, default_config[key])
-            # now make sure everything is of the proper type
-            for key, val in context.items():
-                if key not in default_config:
-                    raise KeyError('context has unknown key %s' % key)
-                needed = type(default_config[key])
-                if not isinstance(val, needed):
-                    raise TypeError('context["%s"] is of incorrect type (got '
-                                    '%s need %s)' % (key, type(val), needed))
-        else:
-            raise TypeError('context must be a dict or SharedContext from '
-                            'a Canvas with the same backend, not %s'
-                            % type(context))
-        outs.append(context)
-        outs.append(kwargs.get('vispy_canvas', None))
-        return outs
-
-    def _vispy_init(self):
-        # For any __init__-like actions that must occur *after*
-        # self._vispy_canvas._backend is not None
-
-        # Most backends won't need this. However, there are exceptions.
-        # e.g., pyqt4 with show=True, "show" can't be done until this property
-        # exists because it might call on_draw which might in turn call
-        # canvas.size... which relies on canvas._backend being set.
-        pass
+            out[key] = kwargs[key]
+        return out
 
     def _vispy_set_current(self):
         # Make this the current context
@@ -154,6 +135,12 @@ class BaseCanvasBackend(object):
         # Should return widget size
         raise NotImplementedError()
 
+    def _vispy_get_physical_size(self):
+        # Should return physical widget size (actual number of screen pixels).
+        # This may differ from _vispy_get_size on backends that expose HiDPI
+        # screens. If not overriden, return the logical sizeself.
+        return self._vispy_get_size()
+
     def _vispy_get_position(self):
         # Should return widget position
         raise NotImplementedError()
@@ -173,20 +160,26 @@ class BaseCanvasBackend(object):
         # Most backends would not need to implement this
         return self
 
-    def _vispy_mouse_press(self, **kwds):
+    def _vispy_mouse_press(self, **kwargs):
         # default method for delivering mouse press events to the canvas
-        kwds.update(self._vispy_mouse_data)
-        ev = self._vispy_canvas.events.mouse_press(**kwds)
+        kwargs.update(self._vispy_mouse_data)
+        ev = self._vispy_canvas.events.mouse_press(**kwargs)
         if self._vispy_mouse_data['press_event'] is None:
             self._vispy_mouse_data['press_event'] = ev
 
         self._vispy_mouse_data['buttons'].append(ev.button)
         self._vispy_mouse_data['last_event'] = ev
+
+        if not getattr(self, '_double_click_supported', False):
+            # double-click events are not supported by this backend, so we
+            # detect them manually
+            self._vispy_detect_double_click(ev)
+
         return ev
 
-    def _vispy_mouse_move(self, **kwds):
+    def _vispy_mouse_move(self, **kwargs):
         # default method for delivering mouse move events to the canvas
-        kwds.update(self._vispy_mouse_data)
+        kwargs.update(self._vispy_mouse_data)
 
         # Break the chain of prior mouse events if no buttons are pressed
         # (this means that during a mouse drag, we have full access to every
@@ -196,23 +189,63 @@ class BaseCanvasBackend(object):
             if last_event is not None:
                 last_event._forget_last_event()
         else:
-            kwds['button'] = self._vispy_mouse_data['press_event'].button
+            kwargs['button'] = self._vispy_mouse_data['press_event'].button
 
-        ev = self._vispy_canvas.events.mouse_move(**kwds)
+        ev = self._vispy_canvas.events.mouse_move(**kwargs)
         self._vispy_mouse_data['last_event'] = ev
         return ev
 
-    def _vispy_mouse_release(self, **kwds):
+    def _vispy_mouse_release(self, **kwargs):
         # default method for delivering mouse release events to the canvas
-        kwds.update(self._vispy_mouse_data)
-        ev = self._vispy_canvas.events.mouse_release(**kwds)
-        if ev.button == self._vispy_mouse_data['press_event'].button:
+        kwargs.update(self._vispy_mouse_data)
+
+        ev = self._vispy_canvas.events.mouse_release(**kwargs)
+        if (self._vispy_mouse_data['press_event']
+                and self._vispy_mouse_data['press_event'].button == ev.button):
             self._vispy_mouse_data['press_event'] = None
 
-        self._vispy_mouse_data['buttons'].remove(ev.button)
+        if ev.button in self._vispy_mouse_data['buttons']:
+            self._vispy_mouse_data['buttons'].remove(ev.button)
         self._vispy_mouse_data['last_event'] = ev
+
         return ev
 
+    def _vispy_mouse_double_click(self, **kwargs):
+        # default method for delivering double-click events to the canvas
+        kwargs.update(self._vispy_mouse_data)
+
+        ev = self._vispy_canvas.events.mouse_double_click(**kwargs)
+        self._vispy_mouse_data['last_event'] = ev
+        return ev
+
+    def _vispy_detect_double_click(self, ev, **kwargs):
+        # Called on every mouse_press or mouse_release, and calls
+        # _vispy_mouse_double_click if a double-click is calculated.
+        # Should be overridden with an empty function on backends which
+        # natively support double-clicking.
+
+        dt_max = 0.3  # time in seconds for a double-click detection
+
+        lastev = self._vispy_mouse_data['last_mouse_press']
+
+        if lastev is None:
+            self._vispy_mouse_data['last_mouse_press'] = ev
+            return
+
+        assert lastev.type == 'mouse_press'
+        assert ev.type == 'mouse_press'
+
+        # For a double-click to be detected, the button should be the same,
+        # the position should be the same, and the two mouse-presses should
+        # be within dt_max.
+        if ((ev.time - lastev.time <= dt_max) &
+           (lastev.pos[0] - ev.pos[0] == 0) &
+           (lastev.pos[1] - ev.pos[1] == 0) &
+           (lastev.button == ev.button)):
+            self._vispy_mouse_double_click(**kwargs)
+
+        self._vispy_mouse_data['last_mouse_press'] = ev
+
 
 class BaseTimerBackend(object):
     """BaseTimerBackend(vispy_timer)
@@ -235,18 +268,3 @@ class BaseTimerBackend(object):
         # Should return the native timer object
         # Most backends would not need to implement this
         return self
-
-
-class BaseSharedContext(object):
-    """An object encapsulating data necessary for a shared OpenGL context
-
-    The data are backend dependent."""
-    def __init__(self, value):
-        self._value = value
-
-    @property
-    def value(self):
-        return self._value
-
-    def __repr__(self):
-        return ("<SharedContext for %s backend" % self._backend)
diff --git a/vispy/app/canvas.py b/vispy/app/canvas.py
index f4c1dd3..5cff4c1 100644
--- a/vispy/app/canvas.py
+++ b/vispy/app/canvas.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division, print_function
@@ -10,27 +10,21 @@ from time import sleep
 
 from ..util.event import EmitterGroup, Event, WarningEmitter
 from ..util.ptime import time
+from ..util.dpi import get_dpi
+from ..util import config as util_config
 from ..ext.six import string_types
 from . import Application, use_app
-from ._config import get_default_config
+from ..gloo.context import (GLContext, set_current_canvas, forget_canvas)
+
 
 # todo: add functions for asking about current mouse/keyboard state
 # todo: add hover enter/exit events
 # todo: add focus events
 
 
-def _gloo_initialize(event):
-    from ..gloo import gl_initialize
-    gl_initialize()
-
-
 class Canvas(object):
     """Representation of a GUI element with an OpenGL context
 
-    Receives the following events:
-    initialize, resize, draw, mouse_press, mouse_release, mouse_move,
-    mouse_wheel, key_press, key_release, stylus, touch, close
-
     Parameters
     ----------
     title : str
@@ -52,24 +46,22 @@ class Canvas(object):
         Note the canvas application can be accessed at ``canvas.app``.
     create_native : bool
         Whether to create the widget immediately. Default True.
-    init_gloo : bool
-        Initialize standard values in gloo (e.g., ``GL_POINT_SPRITE``).
     vsync : bool
         Enable vertical synchronization.
     resizable : bool
         Allow the window to be resized.
     decorate : bool
-        Decorate the window.
+        Decorate the window. Default True.
     fullscreen : bool | int
         If False, windowed mode is used (default). If True, the default
         monitor is used. If int, the given monitor number is used.
-    context : dict | instance SharedContext | None
-        OpenGL configuration to use when creating the context for the canvas,
-        or a context to share. If None, ``vispy.app.get_default_config`` will
-        be used to set the OpenGL context parameters. Alternatively, the
-        ``canvas.context`` property from an existing canvas (using the
-        same backend) will return a ``SharedContext`` that can be used,
-        thereby sharing the existing context.
+    config : dict
+        A dict with OpenGL configuration options, which is combined
+        with the default configuration options and used to initialize
+        the context. See ``canvas.context.config`` for possible
+        options.
+    shared : Canvas | GLContext | None
+        An existing canvas or context to share OpenGL objects with.
     keys : str | dict | None
         Default key mapping to use. If 'interactive', escape and F11 will
         close the canvas and toggle full-screen mode, respectively.
@@ -78,21 +70,55 @@ class Canvas(object):
         be callable.
     parent : widget-object
         The parent widget if this makes sense for the used backend.
+    dpi : float | None
+        Resolution in dots-per-inch to use for the canvas. If dpi is None,
+        then the value will be determined by querying the global config first,
+        and then the operating system.
+    always_on_top : bool
+        If True, try to create the window in always-on-top mode.
+    px_scale : int > 0
+        A scale factor to apply between logical and physical pixels in addition
+        to the actual scale factor determined by the backend. This option
+        allows the scale factor to be adjusted for testing.
+
+    Notes
+    -----
+    The `Canvas` receives the following events:
+
+        * initialize
+        * resize
+        * draw
+        * mouse_press
+        * mouse_release
+        * mouse_double_click
+        * mouse_move
+        * mouse_wheel
+        * key_press
+        * key_release
+        * stylus
+        * touch
+        * close
+
+    The ordering of the mouse_double_click, mouse_press, and mouse_release
+    events are not guaranteed to be consistent between backends. Only certain
+    backends natively support double-clicking (currently Qt and WX); on other
+    backends, they are detected manually with a fixed time delay.
+    This can cause problems with accessibility, as increasing the OS detection
+    time or using a dedicated double-click button will not be respected.
     """
 
     def __init__(self, title='Vispy canvas', size=(800, 600), position=None,
                  show=False, autoswap=True, app=None, create_native=True,
-                 init_gloo=True, vsync=False, resizable=True, decorate=True,
-                 fullscreen=False, context=None, keys=None, parent=None):
+                 vsync=False, resizable=True, decorate=True, fullscreen=False,
+                 config=None, shared=None, keys=None, parent=None, dpi=None,
+                 always_on_top=False, px_scale=1):
 
-        size = [int(s) for s in size]
+        size = [int(s) * px_scale for s in size]
         if len(size) != 2:
             raise ValueError('size must be a 2-element list')
         title = str(title)
         if not isinstance(fullscreen, (bool, int)):
             raise TypeError('fullscreen must be bool or int')
-        if context is None:
-            context = get_default_config()
 
         # Initialize some values
         self._autoswap = autoswap
@@ -103,6 +129,13 @@ class Canvas(object):
         self._fps_callback = None
         self._backend = None
         self._closed = False
+        self._px_scale = int(px_scale)
+
+        if dpi is None:
+            dpi = util_config['dpi']
+        if dpi is None:
+            dpi = get_dpi(raise_error=False)
+        self.dpi = dpi
 
         # Create events
         self.events = EmitterGroup(source=self,
@@ -111,6 +144,7 @@ class Canvas(object):
                                    draw=DrawEvent,
                                    mouse_press=MouseEvent,
                                    mouse_release=MouseEvent,
+                                   mouse_double_click=MouseEvent,
                                    mouse_move=MouseEvent,
                                    mouse_wheel=MouseEvent,
                                    key_press=KeyEvent,
@@ -128,21 +162,9 @@ class Canvas(object):
         self.events.add(paint=emitter)
         self.events.draw.connect(self.events.paint)
 
-        # Initialize gloo settings
-        if init_gloo:
-            self.events.initialize.connect(_gloo_initialize,
-                                           ref='gloo_initialize')
-
-        # store arguments that get set on Canvas init
-        kwargs = dict(title=title, size=size, position=position, show=show,
-                      vsync=vsync, resizable=resizable, decorate=decorate,
-                      fullscreen=fullscreen, context=context, parent=parent,
-                      vispy_canvas=self)
-        self._backend_kwargs = kwargs
-
         # Get app instance
         if app is None:
-            self._app = use_app()
+            self._app = use_app(call_reuse=False)
         elif isinstance(app, Application):
             self._app = app
         elif isinstance(app, string_types):
@@ -150,13 +172,39 @@ class Canvas(object):
         else:
             raise ValueError('Invalid value for app %r' % app)
 
+        # Check shared and context
+        if shared is None:
+            pass
+        elif isinstance(shared, Canvas):
+            shared = shared.context.shared
+        elif isinstance(shared, GLContext):
+            shared = shared.shared
+        else:
+            raise TypeError('shared must be a Canvas, not %s' % type(shared))
+        config = config or {}
+        if not isinstance(config, dict):
+            raise TypeError('config must be a dict, not %s' % type(config))
+
+        # Create new context
+        self._context = GLContext(config, shared)
+
         # Deal with special keys
         self._set_keys(keys)
 
+        # store arguments that get set on Canvas init
+        kwargs = dict(title=title, size=size, position=position, show=show,
+                      vsync=vsync, resizable=resizable, decorate=decorate,
+                      fullscreen=fullscreen, context=self._context,
+                      parent=parent, always_on_top=always_on_top)
+        self._backend_kwargs = kwargs
+
         # Create widget now (always do this *last*, after all err checks)
         if create_native:
             self.create_native()
 
+            # Now we're ready to become current
+            self.set_current()
+
         if '--vispy-fps' in sys.argv:
             self.measure_fps()
 
@@ -169,22 +217,16 @@ class Canvas(object):
         # Make sure that the app is active
         assert self._app.native
         # Instantiate the backend with the right class
-        be = self._app.backend_module.CanvasBackend(**self._backend_kwargs)
-        self._set_backend(be)
+        self._app.backend_module.CanvasBackend(self, **self._backend_kwargs)
+        # self._backend = set by BaseCanvasBackend
+        self._backend_kwargs = None  # Clean up
 
-    def _set_backend(self, backend):
-        """ Set backend<->canvas references and autoswap
-        """
-        # NOTE: Do *not* combine this with create_native above, since
-        # this private function is used to embed Qt widgets
-        assert backend is not None  # should never happen
-        self._backend = backend
+        # Connect to draw event (append to the end)
+        # Process GLIR commands at each paint event
+        self.events.draw.connect(self.context.flush_commands, position='last')
         if self._autoswap:
-            # append to the end
             self.events.draw.connect((self, 'swap_buffers'),
                                      ref=True, position='last')
-        self._backend._vispy_canvas = self  # it's okay to set this again
-        self._backend._vispy_init()
 
     def _set_keys(self, keys):
         if keys is not None:
@@ -217,16 +259,20 @@ class Canvas(object):
             self._keys_check = keys
 
             def keys_check(event):
-                use_name = event.key.name.lower()
-                if use_name in self._keys_check:
-                    self._keys_check[use_name]()
+                if event.key is not None:
+                    use_name = event.key.name.lower()
+                    if use_name in self._keys_check:
+                        self._keys_check[use_name]()
             self.events.key_press.connect(keys_check, ref=True)
 
     @property
     def context(self):
         """ The OpenGL context of the native widget
+
+        It gives access to OpenGL functions to call on this canvas object,
+        and to the shared context namespace.
         """
-        return self._backend._vispy_context
+        return self._context
 
     @property
     def app(self):
@@ -240,12 +286,30 @@ class Canvas(object):
         """
         return self._backend._vispy_get_native_canvas()
 
+    @property
+    def dpi(self):
+        """ The physical resolution of the canvas in dots per inch.
+        """
+        return self._dpi
+
+    @dpi.setter
+    def dpi(self, dpi):
+        self._dpi = float(dpi)
+        self.update()
+
     def connect(self, fun):
-        """ Connect a function to an event. The name of the function
+        """ Connect a function to an event
+
+        The name of the function
         should be on_X, with X the name of the event (e.g. 'on_draw').
 
-        This method is typically used as a decorater on a function
+        This method is typically used as a decorator on a function
         definition for an event handler.
+
+        Parameters
+        ----------
+        fun : callable
+            The function.
         """
         # Get and check name
         name = fun.__name__
@@ -267,11 +331,32 @@ class Canvas(object):
     @property
     def size(self):
         """ The size of canvas/window """
-        return self._backend._vispy_get_size()
+        size = self._backend._vispy_get_size()
+        return (size[0] // self._px_scale, size[1] // self._px_scale)
 
     @size.setter
     def size(self, size):
-        return self._backend._vispy_set_size(size[0], size[1])
+        return self._backend._vispy_set_size(size[0] * self._px_scale,
+                                             size[1] * self._px_scale)
+
+    @property
+    def physical_size(self):
+        """ The physical size of the canvas/window, which may differ from the
+        size property on backends that expose HiDPI """
+        return self._backend._vispy_get_physical_size()
+
+    @property
+    def pixel_scale(self):
+        """ The ratio between the number of logical pixels, or 'points', and
+        the physical pixels on the device. In most cases this will be 1.0,
+        but on certain backends this will be greater than 1. This should be
+        used as a scaling factor when writing your own visualisations
+        with Gloo (make a copy and multiply all your logical pixel values
+        by it) but you should rarely, if ever, need to use this in your own
+        Visuals or SceneGraph visualisations; instead you should apply the
+        canvas_fb_transform in the SceneGraph canvas. """
+
+        return self._px_scale * self.physical_size[0] // self.size[0]
 
     @property
     def fullscreen(self):
@@ -306,34 +391,62 @@ class Canvas(object):
     # ----------------------------------------------------------------- fps ---
     @property
     def fps(self):
-        """ The fps of canvas/window, measured as the rate that events.draw
-        is emitted. """
+        """The fps of canvas/window, as the rate that events.draw is emitted
+        """
         return self._fps
 
+    def set_current(self, event=None):
+        """Make this the active GL canvas
+
+        Parameters
+        ----------
+        event : None
+            Not used.
+        """
+        self._backend._vispy_set_current()
+        set_current_canvas(self)
+
     def swap_buffers(self, event=None):
-        """ Swap GL buffers such that the offscreen buffer becomes visible.
+        """Swap GL buffers such that the offscreen buffer becomes visible
+
+        Parameters
+        ----------
+        event : None
+            Not used.
         """
         self._backend._vispy_swap_buffers()
 
-    def show(self, visible=True):
-        """ Show (or hide) the canvas """
-        return self._backend._vispy_set_visible(visible)
+    def show(self, visible=True, run=False):
+        """Show or hide the canvas
+
+        Parameters
+        ----------
+        visible : bool
+            Make the canvas visible.
+        run : bool
+            Run the backend event loop.
+        """
+        self._backend._vispy_set_visible(visible)
+        if run:
+            self.app.run()
 
     def update(self, event=None):
-        """ Inform the backend that the Canvas needs to be redrawn
-        
-        This method accepts an optional ``event`` argument so it can be used
-        as an event handler (the argument is ignored). 
+        """Inform the backend that the Canvas needs to be redrawn
+
+        Parameters
+        ----------
+        event : None
+            Not used.
         """
         if self._backend is not None:
-            return self._backend._vispy_update()
-        else:
-            return
+            self._backend._vispy_update()
 
     def close(self):
-        """ Close the canvas
+        """Close the canvas
 
-        Note: This will usually destroy the GL context. For Qt, the context
+        Notes
+        -----
+        This will usually destroy the GL context. For Qt, the context
         (and widget) will be destroyed only if the widget is top-level.
         To avoid having the widget destroyed (more like standard Qt
         behavior), consider making the widget a sub-widget.
@@ -342,11 +455,10 @@ class Canvas(object):
             self._closed = True
             self.events.close()
             self._backend._vispy_close()
+        forget_canvas(self)
 
     def _update_fps(self, event):
-        """ Updates the fps after every window and resets the basetime
-        and frame count to current time and 0, respectively
-        """
+        """Update the fps after every window"""
         self._frame_count += 1
         diff = time() - self._basetime
         if (diff > self._fps_window):
@@ -359,7 +471,7 @@ class Canvas(object):
         """Measure the current FPS
 
         Sets the update window, connects the draw event to update_fps
-        and sets the callback function. 
+        and sets the callback function.
 
         Parameters
         ----------
@@ -368,7 +480,7 @@ class Canvas(object):
         callback : function | str
             The function to call with the float FPS value, or the string
             to be formatted with the fps value and then printed. The
-            default is '%1.1f FPS'. If callback evaluates to False, the
+            default is ``'%1.1f FPS'``. If callback evaluates to False, the
             FPS measurement is stopped.
         """
         # Connect update_fps function to draw
@@ -376,7 +488,10 @@ class Canvas(object):
         if callback:
             if isinstance(callback, string_types):
                 callback_str = callback  # because callback gets overwritten
-                callback = lambda x: print(callback_str % x)
+
+                def callback(x):
+                    print(callback_str % x)
+
             self._fps_window = window
             self.events.draw.connect(self._update_fps)
             self._fps_callback = callback
@@ -385,8 +500,9 @@ class Canvas(object):
 
     # ---------------------------------------------------------------- misc ---
     def __repr__(self):
-        return ('<Vispy canvas (%s backend) at %s>'
-                % (self.app.backend_name, hex(id(self))))
+        return ('<%s (%s) at %s>'
+                % (self.__class__.__name__,
+                   self.app.backend_name, hex(id(self))))
 
     def __enter__(self):
         self.show()
@@ -396,64 +512,19 @@ class Canvas(object):
     def __exit__(self, type, value, traceback):
         # ensure all GL calls are complete
         if not self._closed:
-            from ..gloo import gl
             self._backend._vispy_set_current()
-            gl.glFinish()
+            self.context.finish()
             self.close()
         sleep(0.1)  # ensure window is really closed/destroyed
 
-    # def mouse_event(self, event):
-        #"""Called when a mouse input event has occurred (the mouse has moved,
-        # a button was pressed/released, or the wheel has moved)."""
-
-    # def key_event(self, event):
-        #"""Called when a keyboard event has occurred (a key was pressed or
-        # released while the canvas has focus)."""
-
-    # def touch_event(self, event):
-        #"""Called when the user touches the screen over a Canvas.
-
-        # Event properties:
-        #     event.touches
-        #     [ (x,y,pressure), ... ]
-        #"""
-
-    # def stylus_event(self, event):
-        #"""Called when a stylus has been used to interact with the Canvas.
-
-        # Event properties:
-        #     event.device
-        #     event.pos  (x,y)
-        #     event.pressure
-        #     event.angle
-        #"""
-
-    # def initialize_event(self, event):
-        #"""Called when the OpenGL context is initialy made available for this
-        # Canvas."""
-
-    # def resize_event(self, event):
-        #"""Called when the Canvas is resized.
-
-        # Event properties:
-        #     event.size  (w,h)
-        #"""
-
-    # def draw_event(self, event):
-        #"""Called when all or part of the Canvas needs to be redrawn.
-
-        # Event properties:
-        #     event.region  (x,y,w,h) region of Canvas requiring redraw
-        #"""
-
 
 # Event subclasses specific to the Canvas
 class MouseEvent(Event):
-
     """Mouse event class
 
     Note that each event object has an attribute for each of the input
-    arguments listed below.
+    arguments listed below, as well as a "time" attribute with the event's
+    precision start time.
 
     Parameters
     ----------
@@ -483,22 +554,22 @@ class MouseEvent(Event):
         allowing the entire drag to be reconstructed.
     native : object (optional)
        The native GUI event object
-    **kwds : keyword arguments
+    **kwargs : keyword arguments
         All extra keyword arguments become attributes of the event object.
-
     """
 
     def __init__(self, type, pos=None, button=None, buttons=None,
                  modifiers=None, delta=None, last_event=None, press_event=None,
-                 **kwds):
-        Event.__init__(self, type, **kwds)
-        self._pos = (0, 0) if (pos is None) else (pos[0], pos[1])
+                 **kwargs):
+        Event.__init__(self, type, **kwargs)
+        self._pos = np.array([0, 0]) if (pos is None) else np.array(pos)
         self._button = int(button) if (button is not None) else None
         self._buttons = [] if (buttons is None) else buttons
         self._modifiers = tuple(modifiers or ())
-        self._delta = (0.0, 0.0) if (delta is None) else (delta[0], delta[1])
+        self._delta = np.zeros(2) if (delta is None) else np.array(delta)
         self._last_event = last_event
         self._press_event = press_event
+        self._time = time()
 
     @property
     def pos(self):
@@ -528,6 +599,10 @@ class MouseEvent(Event):
     def last_event(self):
         return self._last_event
 
+    @property
+    def time(self):
+        return self._time
+
     def _forget_last_event(self):
         # Needed to break otherwise endless last-event chains
         self._last_event = None
@@ -575,7 +650,6 @@ class MouseEvent(Event):
 
 
 class KeyEvent(Event):
-
     """Key event class
 
     Note that each event object has an attribute for each of the input
@@ -594,12 +668,12 @@ class KeyEvent(Event):
         time of the event (shift, control, alt, meta).
     native : object (optional)
        The native GUI event object
-    **kwds : keyword arguments
+    **kwargs : keyword arguments
         All extra keyword arguments become attributes of the event object.
     """
 
-    def __init__(self, type, key=None, text='', modifiers=None, **kwds):
-        Event.__init__(self, type, **kwds)
+    def __init__(self, type, key=None, text='', modifiers=None, **kwargs):
+        Event.__init__(self, type, **kwargs)
         self._key = key
         self._text = text
         self._modifiers = tuple(modifiers or ())
@@ -618,8 +692,7 @@ class KeyEvent(Event):
 
 
 class ResizeEvent(Event):
-
-    """ Resize event class
+    """Resize event class
 
     Note that each event object has an attribute for each of the input
     arguments listed below.
@@ -629,25 +702,34 @@ class ResizeEvent(Event):
     type : str
        String indicating the event type (e.g. mouse_press, key_release)
     size : (int, int)
-        The new size of the Canvas.
+        The new size of the Canvas, in points (logical pixels).
+    physical_size : (int, int)
+        The new physical size of the Canvas, in pixels.
     native : object (optional)
        The native GUI event object
-    **kwds : extra keyword arguments
+    **kwargs : extra keyword arguments
         All extra keyword arguments become attributes of the event object.
     """
 
-    def __init__(self, type, size=None, **kwds):
-        Event.__init__(self, type, **kwds)
+    def __init__(self, type, size=None, physical_size=None, **kwargs):
+        Event.__init__(self, type, **kwargs)
         self._size = tuple(size)
+        if physical_size is None:
+            self._physical_size = self._size
+        else:
+            self._physical_size = tuple(physical_size)
 
     @property
     def size(self):
         return self._size
 
+    @property
+    def physical_size(self):
+        return self._physical_size
+
 
 class DrawEvent(Event):
-
-    """ Draw event class
+    """Draw event class
 
     This type of event is sent to Canvas.events.draw when a redraw
     is required.
@@ -664,12 +746,12 @@ class DrawEvent(Event):
         If None, the entire canvas must be redrawn.
     native : object (optional)
        The native GUI event object
-    **kwds : extra keyword arguments
+    **kwargs : extra keyword arguments
         All extra keyword arguments become attributes of the event object.
     """
 
-    def __init__(self, type, region=None, **kwds):
-        Event.__init__(self, type, **kwds)
+    def __init__(self, type, region=None, **kwargs):
+        Event.__init__(self, type, **kwargs)
         self._region = region
 
     @property
diff --git a/vispy/app/inputhook.py b/vispy/app/inputhook.py
new file mode 100644
index 0000000..73aafaf
--- /dev/null
+++ b/vispy/app/inputhook.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+"""
+Support for interactive mode to allow VisPy's event loop to be run alongside
+a console terminal, without using threads.  This code relies on inputhooks
+built-in to the Python interpreter, and supports IPython too. The underlying
+inputhook implementation is from IPython 3.x.
+
+Note that IPython notebook integration is not supported, as the browser does
+not use Python's PyOS_InputHook functionality.
+"""
+
+from ..ext.ipy_inputhook import inputhook_manager, InputHookBase, stdin_ready
+
+from time import sleep
+from ..util.ptime import time
+
+
+def set_interactive(enabled=True, app=None):
+    """Activate the IPython hook for VisPy.  If the app is not specified, the
+    default is used.
+    """
+    if enabled:
+        inputhook_manager.enable_gui('vispy', app)
+    else:
+        inputhook_manager.disable_gui()
+
+
+ at inputhook_manager.register('vispy')
+class VisPyInputHook(InputHookBase):
+    """Implementation of an IPython 3.x InputHook for VisPy.  This is loaded
+    by default when you call vispy.app.run() in a console-based interactive
+    session, but you can also trigger it manually by importing this module
+    then typing:
+        >>> %enable_gui vispy
+    """
+
+    def enable(self, app=None):
+        """Activate event loop integration with this VisPy application.
+
+        Parameters
+        ----------
+        app : instance of Application
+           The VisPy application that's being used.  If None, then the
+           default application is retrieved.
+
+        Notes
+        -----
+        This methods sets the ``PyOS_InputHook`` to this implementation,
+        which allows Vispy to integrate with terminal-based applications
+        running in interactive mode (Python or IPython).
+        """
+
+        from .. import app as _app
+        self.app = app or _app.use_app()
+        self.manager.set_inputhook(self._vispy_inputhook)
+        return app
+
+    def _vispy_inputhook(self):
+        try:
+            t = time()
+            while not stdin_ready():
+                self.app.process_events()
+
+                used_time = time() - t
+                if used_time > 10.0:
+                    sleep(1.0)
+                elif used_time > 0.1:
+                    sleep(0.05)
+                else:
+                    sleep(0.001)
+        except KeyboardInterrupt:
+            pass
+        return 0
diff --git a/vispy/app/qt.py b/vispy/app/qt.py
new file mode 100644
index 0000000..c75480a
--- /dev/null
+++ b/vispy/app/qt.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+# Force the selection of an application backend. If the user has already
+# imported PyQt or PySide, this should result in selection of the corresponding
+# backend.
+from . import use_app
+app = use_app()
+try:
+    QtGui = app.backend_module.QtGui
+except AttributeError:
+    raise RuntimeError("Cannot import Qt library; non-Qt backend is already "
+                       "in use.")
+
+
+class QtCanvas(QtGui.QWidget):
+    """ Qt widget containing a vispy Canvas. 
+    
+    This is a convenience class that allows a vispy canvas to be embedded
+    directly into a Qt application.
+    All methods and properties of the Canvas are wrapped by this class.
+    
+    Parameters
+    ----------
+    parent : QWidget or None
+        The Qt parent to assign to this widget.
+    canvas : instance or subclass of Canvas
+        The vispy Canvas to display inside this widget, or a Canvas subclass
+        to instantiate using any remaining keyword arguments.
+    """
+    
+    def __init__(self, parent=None, canvas=None, **kwargs):
+        from .canvas import Canvas
+        if canvas is None:
+            canvas = Canvas
+        if issubclass(canvas, Canvas):
+            canvas = canvas(**kwargs)
+        elif len(**kwargs) > 0:
+            raise TypeError('Invalid keyword arguments: %s' % 
+                            list(kwargs.keys()))
+        if not isinstance(canvas, Canvas):
+            raise TypeError('canvas argument must be an instance or subclass '
+                            'of Canvas.')
+            
+        QtGui.QWidget.__init__(self, parent)
+        self.layout = QtGui.QGridLayout()
+        self.setLayout(self.layout)
+        self.layout.setContentsMargins(0, 0, 0, 0)
+        self._canvas = canvas
+        self.layout.addWidget(canvas.native)
+        self.setSizePolicy(canvas.native.sizePolicy())
+        
+    def __getattr__(self, attr):
+        if hasattr(self._canvas, attr):
+            return getattr(self._canvas, attr)
+        else:
+            raise AttributeError(attr)
+
+    def update(self):
+        """Call update() on both this widget and the internal canvas.
+        """
+        QtGui.QWidget.update(self)
+        self._canvas.update()
+
+
+class QtSceneCanvas(QtCanvas):
+    """ Convenience class embedding a vispy SceneCanvas inside a QWidget.
+    
+    See QtCanvas.
+    """
+    def __init__(self, parent=None, **kwargs):
+        from ..scene.canvas import SceneCanvas
+        QtCanvas.__init__(self, parent, canvas=SceneCanvas, **kwargs)
diff --git a/vispy/app/tests/qt-designer.ui b/vispy/app/tests/qt-designer.ui
index 5e4d3a4..ca59974 100644
--- a/vispy/app/tests/qt-designer.ui
+++ b/vispy/app/tests/qt-designer.ui
@@ -31,7 +31,7 @@
      </property>
      <layout class="QGridLayout" name="gridLayout_2">
       <item row="0" column="0">
-       <widget class="CanvasBackend" name="canvas" native="true">
+       <widget class="QtSceneCanvas" name="canvas" native="true">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
           <horstretch>0</horstretch>
@@ -47,9 +47,9 @@
  </widget>
  <customwidgets>
   <customwidget>
-   <class>CanvasBackend</class>
+   <class>QtSceneCanvas</class>
    <extends>QWidget</extends>
-   <header>vispy.app.backends._qt</header>
+   <header>vispy.app.qt</header>
    <container>1</container>
   </customwidget>
  </customwidgets>
diff --git a/vispy/app/tests/test_app.py b/vispy/app/tests/test_app.py
index 74fd6a5..ffb878b 100644
--- a/vispy/app/tests/test_app.py
+++ b/vispy/app/tests/test_app.py
@@ -1,23 +1,23 @@
 import numpy as np
 import sys
-import os
 from collections import namedtuple
 from time import sleep
 
 from numpy.testing import assert_array_equal
-from nose.tools import assert_equal, assert_true, assert_raises
 
 from vispy.app import use_app, Canvas, Timer, MouseEvent, KeyEvent
 from vispy.app.base import BaseApplicationBackend
-from vispy.testing import requires_application, SkipTest, assert_is, assert_in
+from vispy.testing import (requires_application, SkipTest, assert_is,
+                           assert_in, run_tests_if_main,
+                           assert_equal, assert_true, assert_raises)
 from vispy.util import keys, use_log_level
 
 from vispy.gloo.program import (Program, VertexBuffer, IndexBuffer)
-from vispy.gloo.shader import VertexShader, FragmentShader
 from vispy.gloo.util import _screenshot
 from vispy.gloo import gl
+from vispy.ext.six.moves import StringIO
 
-gl.use_gl('desktop debug')
+gl.use_gl('gl2 debug')
 
 
 def on_nonexist(self, *args):
@@ -97,16 +97,6 @@ def _test_callbacks(canvas):
         backend._on_event(event)
         event.type = 769  # SDL_KEYUP
         backend._on_event(event)
-    elif 'glut' in backend_name.lower():
-        backend.on_mouse_action(0, 0, 0, 0)
-        backend.on_mouse_action(0, 1, 0, 0)
-        backend.on_mouse_action(3, 0, 0, 0)
-        backend.on_draw()
-        backend.on_mouse_motion(1, 1)
-        # Skip keypress tests b/c of glutGetModifiers warning
-        #for key in (100, 'a'):
-        #    backend.on_key_press(key, 0, 0)
-        #    backend.on_key_release(key, 0, 0)
     elif 'wx' in backend_name.lower():
         # Constructing fake wx events is too hard
         pass
@@ -117,11 +107,8 @@ def _test_callbacks(canvas):
 @requires_application()
 def test_run():
     """Test app running"""
-    a = use_app()
-    if a.backend_name.lower() == 'glut':
-        raise SkipTest('cannot test running glut')  # knownfail
     for _ in range(2):
-        with Canvas(size=(100, 100), show=True, title=' run') as c:
+        with Canvas(size=(100, 100), show=True, title='run') as c:
             @c.events.draw.connect
             def draw(event):
                 print(event)  # test event __repr__
@@ -140,7 +127,7 @@ def test_capability():
     good_kwargs = dict()
     bad_kwargs = dict()
     with Canvas() as c:
-        for key, val in c._backend._vispy_capability.items():
+        for key, val in c.app.backend_module.capability.items():
             if key in non_default_vals:
                 if val:
                     good_kwargs[key] = non_default_vals[key]
@@ -162,7 +149,7 @@ def test_application():
     app = use_app()
     print(app)  # __repr__ without app
     app.create()
-    wrong = 'glut' if app.backend_name.lower() != 'glut' else 'pyglet'
+    wrong = 'glfw' if app.backend_name.lower() != 'glfw' else 'pyqt4'
     assert_raises(RuntimeError, use_app, wrong)
     app.process_events()
     print(app)  # test __repr__
@@ -178,6 +165,7 @@ def test_application():
     title = 'default'
     with Canvas(title=title, size=size, app=app, show=True,
                 position=pos) as canvas:
+        context = canvas.context
         assert_true(canvas.create_native() is None)  # should be done already
         assert_is(canvas.app, app)
         assert_true(canvas.native)
@@ -213,12 +201,12 @@ def test_application():
         with use_log_level('info', record=True, print_msg=False) as log:
             olderr = sys.stderr
             try:
-                with open(os.devnull, 'w') as fid:
-                    sys.stderr = fid
+                fid = StringIO()
+                sys.stderr = fid
 
-                    @canvas.events.paint.connect
-                    def fake(event):
-                        pass
+                @canvas.events.paint.connect
+                def fake(event):
+                    pass
             finally:
                 sys.stderr = olderr
         assert_equal(len(log), 1)
@@ -229,43 +217,30 @@ def test_application():
         ss = _screenshot()
         assert_array_equal(ss.shape, size + (4,))
         assert_equal(len(canvas._backend._vispy_get_geometry()), 4)
-        if (app.backend_name.lower() != 'glut' and  # XXX knownfail for Almar
-                sys.platform != 'win32'):  # XXX knownfail for windows
+        if sys.platform != 'win32':  # XXX knownfail for windows
             assert_array_equal(canvas.size, size)
         assert_equal(len(canvas.position), 2)  # XXX knawnfail, doesn't "take"
 
         # GLOO: should have an OpenGL context already, so these should work
-        vert = VertexShader("void main (void) {gl_Position = pos;}")
-        frag = FragmentShader("void main (void) {gl_FragColor = pos;}")
+        vert = "void main (void) {gl_Position = pos;}"
+        frag = "void main (void) {gl_FragColor = pos;}"
         program = Program(vert, frag)
-        assert_raises(RuntimeError, program.activate)
-
-        vert = VertexShader("uniform vec4 pos;"
-                            "void main (void) {gl_Position = pos;}")
-        frag = FragmentShader("uniform vec4 pos;"
-                              "void main (void) {gl_FragColor = pos;}")
+        assert_raises(RuntimeError, program.glir.flush, context.shared.parser)
+        
+        vert = "uniform vec4 pos;\nvoid main (void) {gl_Position = pos;}"
+        frag = "uniform vec4 pos;\nvoid main (void) {gl_FragColor = pos;}"
         program = Program(vert, frag)
         #uniform = program.uniforms[0]
         program['pos'] = [1, 2, 3, 4]
-        program.activate()  # should print
-        #uniform.upload(program)
-        program.detach(vert)
-        program.detach(frag)
-        assert_raises(RuntimeError, program.detach, vert)
-        assert_raises(RuntimeError, program.detach, frag)
-
-        vert = VertexShader("attribute vec4 pos;"
-                            "void main (void) {gl_Position = pos;}")
-        frag = FragmentShader("void main (void) {}")
+        
+        vert = "attribute vec4 pos;\nvoid main (void) {gl_Position = pos;}"
+        frag = "void main (void) {}"
         program = Program(vert, frag)
         #attribute = program.attributes[0]
         program["pos"] = [1, 2, 3, 4]
-        program.activate()
-        #attribute.upload(program)
-        # cannot get element count
-        #assert_raises(RuntimeError, program.draw, 'POINTS')
-
+        
         # use a real program
+        program._glir.clear()
         vert = ("uniform mat4 u_model;"
                 "attribute vec2 a_position; attribute vec4 a_color;"
                 "varying vec4 v_color;"
@@ -295,10 +270,9 @@ def test_application():
         # bad programs
         frag_bad = ("varying vec4 v_colors")  # no semicolon
         program = Program(vert, frag_bad)
-        assert_raises(RuntimeError, program.activate)
+        assert_raises(RuntimeError, program.glir.flush, context.shared.parser)
         frag_bad = None  # no fragment code. no main is not always enough
-        program = Program(vert, frag_bad)
-        assert_raises(ValueError, program.activate)
+        assert_raises(ValueError, Program, vert, frag_bad)
 
         # Timer
         timer = Timer(interval=0.001, connect=on_mouse_move, iterations=2,
@@ -415,3 +389,6 @@ def test_mouse_key_events():
     ke.key
     ke.text
     ke.modifiers
+
+
+run_tests_if_main()
diff --git a/vispy/app/tests/test_backends.py b/vispy/app/tests/test_backends.py
index 2bda21f..ad37d1c 100644
--- a/vispy/app/tests/test_backends.py
+++ b/vispy/app/tests/test_backends.py
@@ -8,12 +8,12 @@ implementation is corect.
 
 """
 
-from nose.tools import assert_raises
 from inspect import getargspec
 
 import vispy
 from vispy import keys
-from vispy.testing import requires_application, assert_in
+from vispy.testing import (requires_application, assert_in, run_tests_if_main,
+                           assert_raises)
 from vispy.app import use_app, Application
 from vispy.app.backends import _template
 
@@ -42,32 +42,36 @@ def _test_module_properties(_module=None):
 
     # For Qt backend, we have a common implementation
     alt_modname = ''
-    if module_fname in ('_pyside', '_pyqt4'):
+    if module_fname in ('_pyside', '_pyqt4', '_pyqt5'):
         alt_modname = _module.__name__.rsplit('.', 1)[0] + '._qt'
 
     # Test that all _vispy_x methods are there.
     exceptions = (
-        '_vispy_init',
         '_vispy_get_native_canvas',
         '_vispy_get_native_timer',
         '_vispy_get_native_app',
+        '_vispy_reuse',
         '_vispy_mouse_move',
         '_vispy_mouse_press',
         '_vispy_mouse_release',
+        '_vispy_mouse_double_click',
+        '_vispy_detect_double_click',
         '_vispy_get_geometry',
+        '_vispy_get_physical_size',
         '_process_backend_kwargs')  # defined in base class
 
+    class KlassRef(vispy.app.base.BaseCanvasBackend):
+        def __init__(self, *args, **kwargs):
+            pass  # Do not call the base class, since it will check for Canvas
     Klass = _module.CanvasBackend
-    KlassRef = vispy.app.base.BaseCanvasBackend
-    base = KlassRef(None, None)
+    base = KlassRef()
     for key in dir(KlassRef):
         if not key.startswith('__'):
             method = getattr(Klass, key)
             if key not in exceptions:
                 print(key)
                 args = [None] * (len(getargspec(method).args) - 1)
-                assert_raises(NotImplementedError, getattr(base, key),
-                              *args)
+                assert_raises(NotImplementedError, getattr(base, key), *args)
                 if hasattr(method, '__module__'):
                     mod_str = method.__module__  # Py3k
                 else:
@@ -106,7 +110,7 @@ def _test_module_properties(_module=None):
 
     # Test that all events seem to be emitted.
     # Get text
-    fname = _module.__file__.strip('c')
+    fname = _module.__file__.rstrip('c')  # "strip" will break windows!
     with open(fname, 'rb') as fid:
         text = fid.read().decode('utf-8')
 
@@ -114,7 +118,8 @@ def _test_module_properties(_module=None):
     # Stylus and touch are ignored because they are not yet implemented.
     # Mouse events are emitted from the CanvasBackend base class.
     ignore = set(['stylus', 'touch', 'mouse_press', 'paint',
-                  'mouse_move', 'mouse_release', 'close'])
+                  'mouse_move', 'mouse_release', 'mouse_double_click',
+                  'detect_double_click', 'close'])
     if module_fname == '_egl':
         ignore += ['key_release', 'key_press']
     eventNames = set(canvas.events._emitters.keys()) - ignore
@@ -135,7 +140,10 @@ def test_template():
                    a._vispy_get_native_app):
         assert_raises(NotImplementedError, method)
 
-    c = _template.CanvasBackend(None)
+    class TemplateCanvasBackend(_template.CanvasBackend):
+        def __init__(self, *args, **kwargs):
+            pass  # Do not call the base class, since it will check for Canvas
+    c = TemplateCanvasBackend()  # _template.CanvasBackend(None)
     print(c._vispy_get_native_canvas())
     for method in (c._vispy_set_current, c._vispy_swap_buffers, c._vispy_close,
                    c._vispy_update, c._vispy_get_size, c._vispy_get_position):
@@ -150,3 +158,6 @@ def test_template():
 def test_actual():
     """Test actual application module"""
     _test_module_properties(None)
+
+
+run_tests_if_main()
diff --git a/vispy/app/tests/test_context.py b/vispy/app/tests/test_context.py
index 7b5fc9c..b927045 100644
--- a/vispy/app/tests/test_context.py
+++ b/vispy/app/tests/test_context.py
@@ -1,11 +1,11 @@
 import os
 import sys
-from nose.tools import assert_equal, assert_raises
 
-from vispy.testing import requires_application, SkipTest
+from vispy.testing import (requires_application, SkipTest, run_tests_if_main,
+                           assert_equal, assert_raises)
 from vispy.app import Canvas, use_app
-from vispy.gloo import (get_gl_configuration, VertexShader, FragmentShader,
-                        Program, check_error)
+from vispy.gloo import get_gl_configuration, Program
+from vispy.gloo.gl import check_error
 
 
 @requires_application()
@@ -15,58 +15,65 @@ def test_context_properties():
     if a.backend_name.lower() == 'pyglet':
         return  # cannot set more than once on Pyglet
     # stereo, double buffer won't work on every sys
-    contexts = [dict(samples=4), dict(stencil_size=8),
-                dict(samples=4, stencil_size=8)]
+    configs = [dict(samples=4), dict(stencil_size=8),
+               dict(samples=4, stencil_size=8)]
     if a.backend_name.lower() != 'glfw':  # glfw *always* double-buffers
-        contexts.append(dict(double_buffer=False, samples=4))
-        contexts.append(dict(double_buffer=False))
+        configs.append(dict(double_buffer=False, samples=4))
+        configs.append(dict(double_buffer=False))
     else:
         assert_raises(RuntimeError, Canvas, app=a,
-                      context=dict(double_buffer=False))
+                      config=dict(double_buffer=False))
     if a.backend_name.lower() == 'sdl2' and os.getenv('TRAVIS') == 'true':
         raise SkipTest('Travis SDL cannot set context')
-    for context in contexts:
-        n_items = len(context)
-        with Canvas(context=context):
-            if os.getenv('TRAVIS', 'false') == 'true':
-                # Travis cannot handle obtaining these values
-                props = context
+    for config in configs:
+        n_items = len(config)
+        with Canvas(config=config):
+            if 'true' in (os.getenv('TRAVIS', ''),
+                          os.getenv('APPVEYOR', '').lower()):
+                # Travis and Appveyor cannot handle obtaining these values
+                props = config
             else:
                 props = get_gl_configuration()
-            assert_equal(len(context), n_items)
-            for key, val in context.items():
+            assert_equal(len(config), n_items)
+            for key, val in config.items():
                 # XXX knownfail for windows samples, and wx (all platforms)
                 if key == 'samples':
                     iswx = a.backend_name.lower() == 'wx'
                     if not (sys.platform.startswith('win') or iswx):
                         assert_equal(val, props[key], key)
-    assert_raises(TypeError, Canvas, context='foo')
-    assert_raises(KeyError, Canvas, context=dict(foo=True))
-    assert_raises(TypeError, Canvas, context=dict(double_buffer='foo'))
+    assert_raises(TypeError, Canvas, config='foo')
+    assert_raises(KeyError, Canvas, config=dict(foo=True))
+    assert_raises(TypeError, Canvas, config=dict(double_buffer='foo'))
 
 
 @requires_application()
 def test_context_sharing():
     """Test context sharing"""
     with Canvas() as c1:
-        vert = VertexShader("uniform vec4 pos;"
-                            "void main (void) {gl_Position = pos;}")
-        frag = FragmentShader("uniform vec4 pos;"
-                              "void main (void) {gl_FragColor = pos;}")
+        vert = "attribute vec4 pos;\nvoid main (void) {gl_Position = pos;}"
+        frag = "void main (void) {gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);}"
         program = Program(vert, frag)
-        program['pos'] = [1, 2, 3, 4]
-        program.activate()  # should print
+        program['pos'] = [(1, 2, 3, 1), (4, 5, 6, 1)]
+        program.draw('points')
 
         def check():
-            program.activate()
+            # Do something to program and see if it worked
+            program['pos'] = [(1, 2, 3, 1), (4, 5, 6, 1)]  # Do command
+            program.draw('points')
             check_error()
 
-        with Canvas() as c:
+        # Check while c1 is active
+        check()
+
+        # Check while c2 is active (with different context)
+        with Canvas() as c2:
             # pyglet always shares
-            if 'pyglet' not in c.app.backend_name.lower():
-                assert_raises(RuntimeError, check)
-        if c1.app.backend_name.lower() in ('glut',):
-            assert_raises(RuntimeError, Canvas, context=c1.context)
-        else:
-            with Canvas(context=c1.context):
-                check()
+            if 'pyglet' not in c2.app.backend_name.lower():
+                assert_raises(Exception, check)
+
+        # Check while c2 is active (with *same* context)
+        with Canvas(shared=c1.context) as c2:
+            assert c1.context.shared is c2.context.shared  # same object
+            check()
+
+run_tests_if_main()
diff --git a/vispy/app/tests/test_interactive.py b/vispy/app/tests/test_interactive.py
new file mode 100644
index 0000000..0579a6b
--- /dev/null
+++ b/vispy/app/tests/test_interactive.py
@@ -0,0 +1,27 @@
+from vispy.testing import run_tests_if_main
+from vispy.app import set_interactive
+from vispy.ext.ipy_inputhook import inputhook_manager
+
+
+# Expect the inputhook_manager to set boolean `_in_event_loop`
+# on instances of this class when enabled.
+class MockApp(object):
+    pass
+
+
+def test_interactive():
+    f = MockApp()
+    set_interactive(enabled=True, app=f)
+
+    assert inputhook_manager._current_gui == 'vispy'
+    assert f._in_event_loop
+    assert 'vispy' in inputhook_manager.apps
+    assert f == inputhook_manager.apps['vispy']
+
+    set_interactive(enabled=False)
+
+    assert inputhook_manager._current_gui is None
+    assert not f._in_event_loop
+
+
+run_tests_if_main()
diff --git a/vispy/app/tests/test_qt.py b/vispy/app/tests/test_qt.py
index 0d74168..51f76a5 100644
--- a/vispy/app/tests/test_qt.py
+++ b/vispy/app/tests/test_qt.py
@@ -1,48 +1,47 @@
-# Import PyQt4, vispy will see this and use that as a backend
-# Also import QtOpenGL, because vispy needs it.
-
-# This is a strange test: vispy does not need designer or uic stuff to run!
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from os import path as op
 import warnings
 
-from vispy.app import Canvas, use_app
-from vispy.testing import requires_application, SkipTest
-from vispy.gloo import gl
+from vispy.testing import requires_application
 
 
 @requires_application('pyqt4', has=['uic'])
 def test_qt_designer():
     """Embed Canvas via Qt Designer"""
-    app = use_app()
-    if 'pyqt4' not in app.backend_name.lower():
-        raise SkipTest('Not using PyQt4 backend')  # wrong backend
-    from PyQt4 import uic
+    from PyQt4 import QtGui, uic
+    app = QtGui.QApplication.instance()
+    if app is None:
+        app = QtGui.QApplication([])
+    
     fname = op.join(op.dirname(__file__), 'qt-designer.ui')
     with warnings.catch_warnings(record=True):  # pyqt4 deprecation warning
         WindowTemplate, TemplateBaseClass = uic.loadUiType(fname)
-    app.create()  # make sure we have an app, or the init will fail
 
     class MainWindow(TemplateBaseClass):
-
         def __init__(self):
             TemplateBaseClass.__init__(self)
-
+            
             self.ui = WindowTemplate()
             self.ui.setupUi(self)
-            self.show()
 
     win = MainWindow()
+    
     try:
+        canvas = win.ui.canvas
+        # test we can access properties of the internal canvas:
+        canvas.central_widget.add_view()
         win.show()
-        canvas = Canvas(create_native=False)
-        canvas._set_backend(win.ui.canvas)
-        canvas.create_native()
-
-        @canvas.events.draw.connect
-        def on_draw(ev):
-            gl.glClearColor(0.0, 0.0, 0.0, 0.0)
-            gl.glClear(gl.GL_COLOR_BUFFER_BIT)
-            canvas.swap_buffers()
+        app.processEvents()
     finally:
         win.close()
+    
+    return win
+
+
+# Don't use run_tests_if_main(), because we want to show the win
+if __name__ == '__main__':
+    win = test_qt_designer()
+    win.show()
diff --git a/vispy/app/tests/test_simultaneous.py b/vispy/app/tests/test_simultaneous.py
index 650c1b3..85da312 100644
--- a/vispy/app/tests/test_simultaneous.py
+++ b/vispy/app/tests/test_simultaneous.py
@@ -2,11 +2,10 @@
 
 import numpy as np
 from numpy.testing import assert_allclose
-from nose.tools import assert_true
 from time import sleep
 
 from vispy.app import use_app, Canvas, Timer
-from vispy.testing import requires_application, SkipTest
+from vispy.testing import requires_application, SkipTest, run_tests_if_main
 from vispy.util.ptime import time
 from vispy.gloo import gl
 from vispy.gloo.util import _screenshot
@@ -45,8 +44,6 @@ def test_multiple_canvases():
     """Testing multiple canvases"""
     n_check = 3
     app = use_app()
-    if app.backend_name.lower() == 'glut':
-        raise SkipTest('glut cannot use multiple canvases')
     with Canvas(app=app, size=_win_size, title='same_0') as c0:
         with Canvas(app=app, size=_win_size, title='same_1') as c1:
             ct = [0, 0]
@@ -70,8 +67,8 @@ def test_multiple_canvases():
             while (ct[0] < n_check or ct[1] < n_check) and time() < timeout:
                 app.process_events()
             print((ct, n_check))
-            assert_true(n_check <= ct[0] <= n_check + 1)
-            assert_true(n_check <= ct[1] <= n_check + 1)
+            assert n_check <= ct[0] <= n_check + 2  # be a bit lenient
+            assert n_check <= ct[1] <= n_check + 2
 
             # check timer
             global timer_ran
@@ -80,12 +77,13 @@ def test_multiple_canvases():
             def on_timer(_):
                 global timer_ran
                 timer_ran = True
-            timeout = time() + 2.0
-            Timer(0.1, app=app, connect=on_timer, iterations=1,
-                  start=True)
-            while not timer_ran and time() < timeout:
-                app.process_events()
-            assert_true(timer_ran)
+            t = Timer(0.1, app=app, connect=on_timer, iterations=1,  # noqa
+                      start=True)
+            app.process_events()
+            sleep(0.5)  # long for slow systems
+            app.process_events()
+            app.process_events()
+            assert timer_ran
 
     if app.backend_name.lower() == 'wx':
         raise SkipTest('wx fails test #2')  # XXX TODO Fix this
@@ -126,3 +124,6 @@ def test_multiple_canvases():
                 _update_process_check(canvas, 255)
                 bgcolors[ci] = [0.25, 0.25, 0.25, 0.25]
                 _update_process_check(canvas, 64)
+
+
+run_tests_if_main()
diff --git a/vispy/app/timer.py b/vispy/app/timer.py
index 144abf0..be82bce 100644
--- a/vispy/app/timer.py
+++ b/vispy/app/timer.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
@@ -31,7 +31,7 @@ class Timer(object):
         The application to attach the timer to.
     """
 
-    def __init__(self, interval='auto', connect=None, iterations=-1, 
+    def __init__(self, interval='auto', connect=None, iterations=-1,
                  start=False, app=None):
         self.events = EmitterGroup(source=self,
                                    start=Event,
@@ -42,20 +42,20 @@ class Timer(object):
 
         # Get app instance
         if app is None:
-            self._app = use_app()
+            self._app = use_app(call_reuse=False)
         elif isinstance(app, Application):
             self._app = app
         elif isinstance(app, string_types):
             self._app = Application(app)
         else:
             raise ValueError('Invalid value for app %r' % app)
-        
+
         # Ensure app has backend app object
         self._app.native
-        
+
         # Instantiate the backed with the right class
         self._backend = self._app.backend_module.TimerBackend(self)
-        
+
         if interval == 'auto':
             interval = 1.0 / 60
         self._interval = float(interval)
@@ -104,7 +104,14 @@ class Timer(object):
         emitting that number of events. If unspecified, then
         the previous value of self.iterations will be used. If the value is
         negative, then the timer will continue running until stop() is called.
+
+        If the timer is already running when this function is called, nothing
+        happens (timer continues running as it did previously, without
+        changing the interval, number of iterations, or emitting a timer
+        start event).
         """
+        if self.running:
+            return  # don't do anything if already running
         self.iter_count = 0
         if interval is not None:
             self.interval = interval
@@ -157,7 +164,8 @@ class Timer(object):
             type='timer_timeout',
             iteration=self.iter_count,
             elapsed=elapsed,
-            dt=dt)
+            dt=dt,
+            count=self.iter_count)
         self.iter_count += 1
 
     def connect(self, callback):
diff --git a/vispy/color/__init__.py b/vispy/color/__init__.py
index ae53d5d..224dcf5 100644
--- a/vispy/color/__init__.py
+++ b/vispy/color/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 Convience interfaces to manipulate colors.
@@ -7,7 +7,11 @@ Convience interfaces to manipulate colors.
 This module provides support for manipulating colors.
 """
 
-__all__ = ['Color', 'ColorArray', 'LinearGradient', 'get_color_names']
+from ._color_dict import get_color_names, get_color_dict  # noqa
+from .color_array import Color, ColorArray
+from .colormap import (Colormap, BaseColormap,  # noqa
+                       get_colormap, get_colormaps)  # noqa
 
-from ._color_dict import get_color_names  # noqa
-from ._color import Color, ColorArray, LinearGradient  # noqa
+__all__ = ['Color', 'ColorArray', 'Colormap', 'BaseColormap',
+           'get_colormap', 'get_colormaps',
+           'get_color_names', 'get_color_dict']
diff --git a/vispy/color/_color_dict.py b/vispy/color/_color_dict.py
index 6b77a8e..2dcef10 100644
--- a/vispy/color/_color_dict.py
+++ b/vispy/color/_color_dict.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 
@@ -9,13 +9,24 @@ def get_color_names():
     Returns
     -------
     names : list
-        List of color names known by vispy.
+        List of color names known by Vispy.
     """
     names = list(_color_dict.keys())
     names.sort()
     return names
 
 
+def get_color_dict():
+    """Get the known colors
+
+    Returns
+    -------
+    color_dict : dict
+        Dict of colors known by Vispy {name: #rgb}.
+    """
+    return _color_dict.copy()
+
+
 # This is used by color functions to translate user strings to colors
 # For now, this is web colors, and all in hex. It will take some simple
 # but annoying refactoring to deal with non-hex entries if we want them.
@@ -24,8 +35,8 @@ def get_color_names():
 # github.com/bahamas10/css-color-names/blob/master/css-color-names.json
 
 _color_dict = {
-    "w": '#FFFFFF',
     "k": '#000000',
+    "w": '#FFFFFF',
     "r": '#FF0000',
     "g": '#00FF00',
     "b": '#0000FF',
diff --git a/vispy/color/_color.py b/vispy/color/color_array.py
similarity index 62%
rename from vispy/color/_color.py
rename to vispy/color/color_array.py
index 3c2e48b..3142b4b 100644
--- a/vispy/color/_color.py
+++ b/vispy/color/color_array.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division  # just to be safe...
@@ -10,6 +10,8 @@ from copy import deepcopy
 from ..ext.six import string_types
 from ..util import logger
 from ._color_dict import _color_dict
+from .color_space import (_hex_to_rgba, _rgb_to_hex, _rgb_to_hsv,  # noqa
+                          _hsv_to_rgb, _rgb_to_lab, _lab_to_rgb)  # noqa
 
 
 ###############################################################################
@@ -35,7 +37,7 @@ def _string_to_rgb(color):
     return color
 
 
-def _user_to_rgba(color, expand=True):
+def _user_to_rgba(color, expand=True, clip=False):
     """Convert color(s) from any set of fmts (str/hex/arr) to RGB(A) array"""
     if color is None:
         color = np.zeros(4, np.float32)
@@ -46,7 +48,7 @@ def _user_to_rgba(color, expand=True):
     # We have to treat this specially
     elif isinstance(color, (list, tuple)):
         if any(isinstance(c, string_types) for c in color):
-            color = [_user_to_rgba(c) for c in color]
+            color = [_user_to_rgba(c, expand=expand, clip=clip) for c in color]
             if any(len(c) > 1 for c in color):
                 raise RuntimeError('could not parse colors, are they nested?')
             color = [c[0] for c in color]
@@ -57,19 +59,14 @@ def _user_to_rgba(color, expand=True):
         color = np.concatenate((color, np.ones((color.shape[0], 1))),
                                axis=1)
     if color.min() < 0 or color.max() > 1:
-        logger.warning('Color will be clipped between 0 and 1: %s' % color)
-        color = np.clip(color, 0, 1)
+        if clip:
+            color = np.clip(color, 0, 1)
+        else:
+            raise ValueError("Color values must be between 0 and 1 (or use "
+                             "clip=True to automatically clip the values).")
     return color
 
 
-def _check_color_dim(val):
-    """Ensure val is Nx(n_col), usually Nx3"""
-    val = np.atleast_2d(val)
-    if val.shape[1] not in (3, 4):
-        raise RuntimeError('Value must have second dimension of size 3 or 4')
-    return val, val.shape[1]
-
-
 def _array_clip_val(val):
     """Helper to turn val into array and clip between 0 and 1"""
     val = np.array(val)
@@ -80,172 +77,9 @@ def _array_clip_val(val):
 
 
 ###############################################################################
-# RGB<->HEX conversion
-
-def _hex_to_rgba(hexs):
-    """Convert hex to rgba, permitting alpha values in hex"""
-    hexs = np.atleast_1d(np.array(hexs, '|U9'))
-    out = np.ones((len(hexs), 4), np.float32)
-    for hi, h in enumerate(hexs):
-        assert isinstance(h, string_types)
-        off = 1 if h[0] == '#' else 0
-        assert len(h) in (6+off, 8+off)
-        e = (len(h)-off) // 2
-        out[hi, :e] = [int(h[i:i+2], 16) / 255.
-                       for i in range(off, len(h), 2)]
-    return out
-
-
-def _rgb_to_hex(rgbs):
-    """Convert rgb to hex triplet"""
-    rgbs, n_dim = _check_color_dim(rgbs)
-    return np.array(['#%02x%02x%02x' % tuple((255*rgb[:3]).astype(np.uint8))
-                     for rgb in rgbs], '|U7')
-
-
-###############################################################################
-# RGB<->HSV conversion
-
-def _rgb_to_hsv(rgbs):
-    """Convert Nx3 or Nx4 rgb to hsv"""
-    rgbs, n_dim = _check_color_dim(rgbs)
-    hsvs = list()
-    for rgb in rgbs:
-        rgb = rgb[:3]  # don't use alpha here
-        idx = np.argmax(rgb)
-        val = rgb[idx]
-        c = val - np.min(rgb)
-        if c == 0:
-            hue = 0
-            sat = 0
-        else:
-            if idx == 0:  # R == max
-                hue = ((rgb[1] - rgb[2]) / c) % 6
-            elif idx == 1:  # G == max
-                hue = (rgb[2] - rgb[0]) / c + 2
-            else:  # B == max
-                hue = (rgb[0] - rgb[1]) / c + 4
-            hue *= 60
-            sat = c / val
-        hsv = [hue, sat, val]
-        hsvs.append(hsv)
-    hsvs = np.array(hsvs, dtype=np.float32)
-    if n_dim == 4:
-        hsvs = np.concatenate((hsvs, rgbs[:, 3]), axis=1)
-    return hsvs
-
-
-def _hsv_to_rgb(hsvs):
-    """Convert Nx3 or Nx4 hsv to rgb"""
-    hsvs, n_dim = _check_color_dim(hsvs)
-    # In principle, we *might* be able to vectorize this, but might as well
-    # wait until a compelling use case appears
-    rgbs = list()
-    for hsv in hsvs:
-        c = hsv[1] * hsv[2]
-        m = hsv[2] - c
-        hp = hsv[0] / 60
-        x = c * (1 - abs(hp % 2 - 1))
-        if 0 <= hp < 1:
-            r, g, b = c, x, 0
-        elif hp < 2:
-            r, g, b = x, c, 0
-        elif hp < 3:
-            r, g, b = 0, c, x
-        elif hp < 4:
-            r, g, b = 0, x, c
-        elif hp < 5:
-            r, g, b = x, 0, c
-        else:
-            r, g, b = c, 0, x
-        rgb = [r + m, g + m, b + m]
-        rgbs.append(rgb)
-    rgbs = np.array(rgbs, dtype=np.float32)
-    if n_dim == 4:
-        rgbs = np.concatenate((rgbs, hsvs[:, 3]), axis=1)
-    return rgbs
-
-
-###############################################################################
-# RGB<->CIELab conversion
-
-# These numbers are adapted from MIT-licensed MATLAB code for
-# Lab<->RGB conversion. They provide an XYZ<->RGB conversion matrices,
-# w/D65 white point normalization built in.
-
-#_rgb2xyz = np.array([[0.412453, 0.357580, 0.180423],
-#                     [0.212671, 0.715160, 0.072169],
-#                     [0.019334, 0.119193, 0.950227]])
-#_white_norm = np.array([0.950456, 1.0, 1.088754])
-#_rgb2xyz /= _white_norm[:, np.newaxis]
-#_rgb2xyz_norm = _rgb2xyz.T
-_rgb2xyz_norm = np.array([[0.43395276, 0.212671, 0.01775791],
-                         [0.37621941, 0.71516, 0.10947652],
-                         [0.18982783, 0.072169, 0.87276557]])
-
-#_xyz2rgb = np.array([[3.240479, -1.537150, -0.498535],
-#                     [-0.969256, 1.875992, 0.041556],
-#                     [0.055648, -0.204043, 1.057311]])
-#_white_norm = np.array([0.950456, 1., 1.088754])
-#_xyz2rgb *= _white_norm[np.newaxis, :]
-_xyz2rgb_norm = np.array([[3.07993271, -1.53715, -0.54278198],
-                          [-0.92123518, 1.875992, 0.04524426],
-                          [0.05289098, -0.204043, 1.15115158]])
-
-
-def _rgb_to_lab(rgbs):
-    rgbs, n_dim = _check_color_dim(rgbs)
-    # convert RGB->XYZ
-    xyz = rgbs[:, :3].copy()  # a misnomer for now but will end up being XYZ
-    over = xyz > 0.04045
-    xyz[over] = ((xyz[over] + 0.055) / 1.055) ** 2.4
-    xyz[~over] /= 12.92
-    xyz = np.dot(xyz, _rgb2xyz_norm)
-    over = xyz > 0.008856
-    xyz[over] = xyz[over] ** (1. / 3.)
-    xyz[~over] = 7.787 * xyz[~over] + 0.13793103448275862
-
-    # Convert XYZ->LAB
-    L = (116. * xyz[:, 1]) - 16
-    a = 500 * (xyz[:, 0] - xyz[:, 1])
-    b = 200 * (xyz[:, 1] - xyz[:, 2])
-    labs = [L, a, b]
-    # Append alpha if necessary
-    if n_dim == 4:
-        labs.append(np.atleast1d(rgbs[:, 3]))
-    labs = np.array(labs, order='F').T  # Becomes 'C' order b/c of .T
-    return labs
-
-
-def _lab_to_rgb(labs):
-    """Convert Nx3 or Nx4 lab to rgb"""
-    # adapted from BSD-licensed work in MATLAB by Mark Ruzon
-    # Based on ITU-R Recommendation BT.709 using the D65
-    labs, n_dim = _check_color_dim(labs)
-
-    # Convert Lab->XYZ (silly indexing used to preserve dimensionality)
-    y = (labs[:, 0] + 16.) / 116.
-    x = (labs[:, 1] / 500.) + y
-    z = y - (labs[:, 2] / 200.)
-    xyz = np.concatenate(([x], [y], [z]))  # 3xN
-    over = xyz > 0.2068966
-    xyz[over] = xyz[over] ** 3.
-    xyz[~over] = (xyz[~over] - 0.13793103448275862) / 7.787
-
-    # Convert XYZ->LAB
-    rgbs = np.dot(_xyz2rgb_norm, xyz).T
-    over = rgbs > 0.0031308
-    rgbs[over] = 1.055 * (rgbs[over] ** (1. / 2.4)) - 0.055
-    rgbs[~over] *= 12.92
-    if n_dim == 4:
-        rgbs = np.concatenate((rgbs, labs[:, 3]), axis=1)
-    rgbs = np.clip(rgbs, 0., 1.)
-    return rgbs
+# Color Array
 
 
-###############################################################################
-# Now for the user-level classes
-
 class ColorArray(object):
     """An array of colors
 
@@ -261,6 +95,11 @@ class ColorArray(object):
         If no alpha is not supplied in ``color`` entry and ``alpha`` is None,
         then this will default to 1.0 (opaque). If float, it will override
         any alpha values in ``color``, if provided.
+    clip : bool
+        Clip the color value.
+    color_space : 'rgb' | 'hsv'
+       'rgb' (default) : color tuples are interpreted as (r, g, b) components.
+       'hsv' : color tuples are interpreted as (h, s, v) components.
 
     Examples
     --------
@@ -274,6 +113,9 @@ class ColorArray(object):
         >>> b = ColorArray('#0000ff')  # hex color
         >>> w = ColorArray()  # defaults to black
         >>> w.rgb = r.rgb + g.rgb + b.rgb
+        >>>hsv_color = ColorArray(color_space="hsv", color=(0, 0, 0.5))
+        >>>hsv_color
+        <ColorArray: 1 color ((0.5, 0.5, 0.5, 1.0))>
         >>> w == ColorArray('white')
         True
         >>> w.alpha = 0
@@ -290,9 +132,21 @@ class ColorArray(object):
     Under the hood, this class stores data in RGBA format suitable for use
     on the GPU.
     """
-    def __init__(self, color='black', alpha=None):
-        """Parse input type, and set attribute"""
-        rgba = _user_to_rgba(color)
+    def __init__(self, color=(0., 0., 0.), alpha=None,
+                 clip=False, color_space='rgb'):
+
+        # if color is RGB, then set the default color to black
+        color = (0,) * 4 if color is None else color
+        if color_space == 'hsv':
+            # if the color space is hsv, convert hsv to rgb
+            color = _hsv_to_rgb(color)
+        elif color_space != 'rgb':
+            raise ValueError('color_space should be either "rgb" or'
+                             '"hsv", it is ' + color_space)
+
+        # Parse input type, and set attribute"""
+        rgba = _user_to_rgba(color, clip=clip)
+
         if alpha is not None:
             rgba[:, 3] = alpha
         self._rgba = None
@@ -347,6 +201,18 @@ class ColorArray(object):
             value = value.rgba
         self._rgba[item] = value
 
+    def extend(self, colors):
+        """Extend a ColorArray with new colors
+
+        Parameters
+        ----------
+        colors : instance of ColorArray
+            The new colors.
+        """
+        colors = ColorArray(colors)
+        self._rgba = np.vstack((self._rgba, colors._rgba))
+        return self
+
     # RGB(A)
     @property
     def rgba(self):
@@ -499,43 +365,6 @@ class ColorArray(object):
         self.rgba = _lab_to_rgb(val)
 
 
-class LinearGradient(ColorArray):
-    """Class to represent linear gradients
-
-    Parameters
-    ----------
-    colors : ColorArray
-        The control points to use as colors.
-    x : array
-        Array of the same length as ``colors`` that give the x-values
-        to use along the axis of the array.
-    """
-    def __init__(self, colors, x):
-        ColorArray.__init__(self, colors)
-        self.gradient_x = x
-
-    @property
-    def gradient_x(self):
-        return self._grad_x.copy()
-
-    @gradient_x.setter
-    def gradient_x(self, val):
-        x = np.array(val, dtype=np.float32)
-        if x.ndim != 1 or x.size != len(self):
-            raise ValueError('x must 1D with the same size as colors (%s), '
-                             'not %s' % (len(self), x.shape))
-        self._grad_x = x
-
-    def __getitem__(self, loc):
-        try:
-            loc = float(loc)
-        except:
-            raise RuntimeError('location could not be converted to float: %s'
-                               % str(loc))
-        rgba = [np.interp(loc, self._grad_x, rr) for rr in self._rgba.T]
-        return np.array(rgba)
-
-
 class Color(ColorArray):
     """A single color
 
@@ -549,12 +378,14 @@ class Color(ColorArray):
         If no alpha is not supplied in ``color`` entry and ``alpha`` is None,
         then this will default to 1.0 (opaque). If float, it will override
         the alpha value in ``color``, if provided.
+    clip : bool
+        If True, clip the color values.
     """
-    def __init__(self, color='black', alpha=None):
+    def __init__(self, color='black', alpha=None, clip=False):
         """Parse input type, and set attribute"""
         if isinstance(color, (list, tuple)):
             color = np.array(color, np.float32)
-        rgba = _user_to_rgba(color)
+        rgba = _user_to_rgba(color, clip=clip)
         if rgba.shape[0] != 1:
             raise ValueError('color must be of correct shape')
         if alpha is not None:
@@ -598,7 +429,10 @@ class Color(ColorArray):
     def lab(self):
         return super(Color, self).lab[0]
 
+    @property
     def is_blank(self):
+        """Boolean indicating whether the color is invisible.
+        """
         return self.rgba[3] == 0
 
     def __repr__(self):
diff --git a/vispy/color/color_space.py b/vispy/color/color_space.py
new file mode 100644
index 0000000..2048631
--- /dev/null
+++ b/vispy/color/color_space.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division  # just to be safe...
+
+import numpy as np
+
+from ..ext.six import string_types
+
+
+###############################################################################
+# Utility functions
+def _check_color_dim(val):
+    """Ensure val is Nx(n_col), usually Nx3"""
+    val = np.atleast_2d(val)
+    if val.shape[1] not in (3, 4):
+        raise RuntimeError('Value must have second dimension of size 3 or 4')
+    return val, val.shape[1]
+
+
+###############################################################################
+# RGB<->HEX conversion
+
+def _hex_to_rgba(hexs):
+    """Convert hex to rgba, permitting alpha values in hex"""
+    hexs = np.atleast_1d(np.array(hexs, '|U9'))
+    out = np.ones((len(hexs), 4), np.float32)
+    for hi, h in enumerate(hexs):
+        assert isinstance(h, string_types)
+        off = 1 if h[0] == '#' else 0
+        assert len(h) in (6+off, 8+off)
+        e = (len(h)-off) // 2
+        out[hi, :e] = [int(h[i:i+2], 16) / 255.
+                       for i in range(off, len(h), 2)]
+    return out
+
+
+def _rgb_to_hex(rgbs):
+    """Convert rgb to hex triplet"""
+    rgbs, n_dim = _check_color_dim(rgbs)
+    return np.array(['#%02x%02x%02x' % tuple((255*rgb[:3]).astype(np.uint8))
+                     for rgb in rgbs], '|U7')
+
+
+###############################################################################
+# RGB<->HSV conversion
+
+def _rgb_to_hsv(rgbs):
+    """Convert Nx3 or Nx4 rgb to hsv"""
+    rgbs, n_dim = _check_color_dim(rgbs)
+    hsvs = list()
+    for rgb in rgbs:
+        rgb = rgb[:3]  # don't use alpha here
+        idx = np.argmax(rgb)
+        val = rgb[idx]
+        c = val - np.min(rgb)
+        if c == 0:
+            hue = 0
+            sat = 0
+        else:
+            if idx == 0:  # R == max
+                hue = ((rgb[1] - rgb[2]) / c) % 6
+            elif idx == 1:  # G == max
+                hue = (rgb[2] - rgb[0]) / c + 2
+            else:  # B == max
+                hue = (rgb[0] - rgb[1]) / c + 4
+            hue *= 60
+            sat = c / val
+        hsv = [hue, sat, val]
+        hsvs.append(hsv)
+    hsvs = np.array(hsvs, dtype=np.float32)
+    if n_dim == 4:
+        hsvs = np.concatenate((hsvs, rgbs[:, 3]), axis=1)
+    return hsvs
+
+
+def _hsv_to_rgb(hsvs):
+    """Convert Nx3 or Nx4 hsv to rgb"""
+    hsvs, n_dim = _check_color_dim(hsvs)
+    # In principle, we *might* be able to vectorize this, but might as well
+    # wait until a compelling use case appears
+    rgbs = list()
+    for hsv in hsvs:
+        c = hsv[1] * hsv[2]
+        m = hsv[2] - c
+        hp = hsv[0] / 60
+        x = c * (1 - abs(hp % 2 - 1))
+        if 0 <= hp < 1:
+            r, g, b = c, x, 0
+        elif hp < 2:
+            r, g, b = x, c, 0
+        elif hp < 3:
+            r, g, b = 0, c, x
+        elif hp < 4:
+            r, g, b = 0, x, c
+        elif hp < 5:
+            r, g, b = x, 0, c
+        else:
+            r, g, b = c, 0, x
+        rgb = [r + m, g + m, b + m]
+        rgbs.append(rgb)
+    rgbs = np.array(rgbs, dtype=np.float32)
+    if n_dim == 4:
+        rgbs = np.concatenate((rgbs, hsvs[:, 3]), axis=1)
+    return rgbs
+
+
+###############################################################################
+# RGB<->CIELab conversion
+
+# These numbers are adapted from MIT-licensed MATLAB code for
+# Lab<->RGB conversion. They provide an XYZ<->RGB conversion matrices,
+# w/D65 white point normalization built in.
+
+#_rgb2xyz = np.array([[0.412453, 0.357580, 0.180423],
+#                     [0.212671, 0.715160, 0.072169],
+#                     [0.019334, 0.119193, 0.950227]])
+#_white_norm = np.array([0.950456, 1.0, 1.088754])
+#_rgb2xyz /= _white_norm[:, np.newaxis]
+#_rgb2xyz_norm = _rgb2xyz.T
+_rgb2xyz_norm = np.array([[0.43395276, 0.212671, 0.01775791],
+                         [0.37621941, 0.71516, 0.10947652],
+                         [0.18982783, 0.072169, 0.87276557]])
+
+#_xyz2rgb = np.array([[3.240479, -1.537150, -0.498535],
+#                     [-0.969256, 1.875992, 0.041556],
+#                     [0.055648, -0.204043, 1.057311]])
+#_white_norm = np.array([0.950456, 1., 1.088754])
+#_xyz2rgb *= _white_norm[np.newaxis, :]
+_xyz2rgb_norm = np.array([[3.07993271, -1.53715, -0.54278198],
+                          [-0.92123518, 1.875992, 0.04524426],
+                          [0.05289098, -0.204043, 1.15115158]])
+
+
+def _rgb_to_lab(rgbs):
+    rgbs, n_dim = _check_color_dim(rgbs)
+    # convert RGB->XYZ
+    xyz = rgbs[:, :3].copy()  # a misnomer for now but will end up being XYZ
+    over = xyz > 0.04045
+    xyz[over] = ((xyz[over] + 0.055) / 1.055) ** 2.4
+    xyz[~over] /= 12.92
+    xyz = np.dot(xyz, _rgb2xyz_norm)
+    over = xyz > 0.008856
+    xyz[over] = xyz[over] ** (1. / 3.)
+    xyz[~over] = 7.787 * xyz[~over] + 0.13793103448275862
+
+    # Convert XYZ->LAB
+    L = (116. * xyz[:, 1]) - 16
+    a = 500 * (xyz[:, 0] - xyz[:, 1])
+    b = 200 * (xyz[:, 1] - xyz[:, 2])
+    labs = [L, a, b]
+    # Append alpha if necessary
+    if n_dim == 4:
+        labs.append(np.atleast1d(rgbs[:, 3]))
+    labs = np.array(labs, order='F').T  # Becomes 'C' order b/c of .T
+    return labs
+
+
+def _lab_to_rgb(labs):
+    """Convert Nx3 or Nx4 lab to rgb"""
+    # adapted from BSD-licensed work in MATLAB by Mark Ruzon
+    # Based on ITU-R Recommendation BT.709 using the D65
+    labs, n_dim = _check_color_dim(labs)
+
+    # Convert Lab->XYZ (silly indexing used to preserve dimensionality)
+    y = (labs[:, 0] + 16.) / 116.
+    x = (labs[:, 1] / 500.) + y
+    z = y - (labs[:, 2] / 200.)
+    xyz = np.concatenate(([x], [y], [z]))  # 3xN
+    over = xyz > 0.2068966
+    xyz[over] = xyz[over] ** 3.
+    xyz[~over] = (xyz[~over] - 0.13793103448275862) / 7.787
+
+    # Convert XYZ->LAB
+    rgbs = np.dot(_xyz2rgb_norm, xyz).T
+    over = rgbs > 0.0031308
+    rgbs[over] = 1.055 * (rgbs[over] ** (1. / 2.4)) - 0.055
+    rgbs[~over] *= 12.92
+    if n_dim == 4:
+        rgbs = np.concatenate((rgbs, labs[:, 3]), axis=1)
+    rgbs = np.clip(rgbs, 0., 1.)
+    return rgbs
diff --git a/vispy/color/colormap.py b/vispy/color/colormap.py
new file mode 100644
index 0000000..0fc5cb4
--- /dev/null
+++ b/vispy/color/colormap.py
@@ -0,0 +1,564 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division  # just to be safe...
+
+import numpy as np
+
+from .color_array import ColorArray
+from ..ext.six import string_types
+from ..ext.cubehelix import cubehelix
+
+###############################################################################
+# Color maps
+
+
+# Utility functions for interpolation in NumPy.
+def _vector_or_scalar(x, type='row'):
+    """Convert an object to either a scalar or a row or column vector."""
+    if isinstance(x, (list, tuple)):
+        x = np.array(x)
+    if isinstance(x, np.ndarray):
+        assert x.ndim == 1
+        if type == 'column':
+            x = x[:, None]
+    return x
+
+
+def _vector(x, type='row'):
+    """Convert an object to a row or column vector."""
+    if isinstance(x, (list, tuple)):
+        x = np.array(x, dtype=np.float32)
+    elif not isinstance(x, np.ndarray):
+        x = np.array([x], dtype=np.float32)
+    assert x.ndim == 1
+    if type == 'column':
+        x = x[:, None]
+    return x
+
+
+def _find_controls(x, controls=None, clip=None):
+    x_controls = np.clip(np.searchsorted(controls, x) - 1, 0, clip)
+    return x_controls.astype(np.int32)
+
+
+# Normalization
+def _normalize(x, cmin=None, cmax=None, clip=True):
+    """Normalize an array from the range [cmin, cmax] to [0,1],
+    with optional clipping."""
+    if not isinstance(x, np.ndarray):
+        x = np.array(x)
+    if cmin is None:
+        cmin = x.min()
+    if cmax is None:
+        cmax = x.max()
+    if cmin == cmax:
+        return .5 * np.ones(x.shape)
+    else:
+        cmin, cmax = float(cmin), float(cmax)
+        y = (x - cmin) * 1. / (cmax - cmin)
+        if clip:
+            y = np.clip(y, 0., 1.)
+        return y
+
+
+# Interpolation functions in NumPy.
+def _mix_simple(a, b, x):
+    """Mix b (with proportion x) with a."""
+    x = np.clip(x, 0.0, 1.0)
+    return (1.0 - x)*a + x*b
+
+
+def _interpolate_multi(colors, x, controls):
+    x = x.ravel()
+    n = len(colors)
+    # For each element in x, the control index of its bin's left boundary.
+    x_step = _find_controls(x, controls, n-2)
+    # The length of each bin.
+    controls_length = np.diff(controls).astype(np.float32)
+    # Prevent division by zero error.
+    controls_length[controls_length == 0.] = 1.
+    # Like x, but relative to each bin.
+    _to_clip = x - controls[x_step]
+    _to_clip /= controls_length[x_step]
+    x_rel = np.clip(_to_clip, 0., 1.)
+    return (colors[x_step],
+            colors[x_step + 1],
+            x_rel[:, None])
+
+
+def mix(colors, x, controls=None):
+    a, b, x_rel = _interpolate_multi(colors, x, controls)
+    return _mix_simple(a, b, x_rel)
+
+
+def smoothstep(edge0, edge1, x):
+    """ performs smooth Hermite interpolation
+        between 0 and 1 when edge0 < x < edge1.  """
+    # Scale, bias and saturate x to 0..1 range
+    x = np.clip((x - edge0)/(edge1 - edge0), 0.0, 1.0)
+    # Evaluate polynomial
+    return x*x*(3 - 2*x)
+
+
+def step(colors, x, controls=None):
+    x = x.ravel()
+    """Step interpolation from a set of colors. x belongs in [0, 1]."""
+    assert (controls[0], controls[-1]) == (0., 1.)
+    ncolors = len(colors)
+    assert ncolors == len(controls) - 1
+    assert ncolors >= 2
+    x_step = _find_controls(x, controls, ncolors-1)
+    return colors[x_step, ...]
+
+
+# GLSL interpolation functions.
+def _glsl_mix(controls=None):
+    """Generate a GLSL template function from a given interpolation patterns
+    and control points."""
+    assert (controls[0], controls[-1]) == (0., 1.)
+    ncolors = len(controls)
+    assert ncolors >= 2
+    if ncolors == 2:
+        s = "    return mix($color_0, $color_1, t);\n"
+    else:
+        s = ""
+        for i in range(ncolors-1):
+            if i == 0:
+                ifs = 'if (t < %.6f)' % (controls[i+1])
+            elif i == (ncolors-2):
+                ifs = 'else'
+            else:
+                ifs = 'else if (t < %.6f)' % (controls[i+1])
+            adj_t = '(t - %s) / %s' % (controls[i],
+                                       controls[i+1] - controls[i])
+            s += ("%s {\n    return mix($color_%d, $color_%d, %s);\n} " %
+                  (ifs, i, i+1, adj_t))
+    return "vec4 colormap(float t) {\n%s\n}" % s
+
+
+def _glsl_step(controls=None):
+    assert (controls[0], controls[-1]) == (0., 1.)
+    ncolors = len(controls) - 1
+    assert ncolors >= 2
+    s = ""
+    for i in range(ncolors-1):
+        if i == 0:
+            ifs = 'if (t < %.6f)' % (controls[i+1])
+        elif i == (ncolors-2):
+            ifs = 'else'
+        else:
+            ifs = 'else if (t < %.6f)' % (controls[i+1])
+        s += """%s {\n    return $color_%d;\n} """ % (ifs, i)
+    return """vec4 colormap(float t) {\n%s\n}""" % s
+
+
+# Mini GLSL template system for colors.
+def _process_glsl_template(template, colors):
+    """Replace $color_i by color #i in the GLSL template."""
+    for i in range(len(colors) - 1, -1, -1):
+        color = colors[i]
+        assert len(color) == 4
+        vec4_color = 'vec4(%.3f, %.3f, %.3f, %.3f)' % tuple(color)
+        template = template.replace('$color_%d' % i, vec4_color)
+    return template
+
+
+class BaseColormap(object):
+    """Class representing a colormap:
+
+        t \in [0, 1] --> rgba_color
+
+    Parameters
+    ----------
+    colors : list of lists, tuples, or ndarrays
+        The control colors used by the colormap (shape = (ncolors, 4)).
+
+    Notes
+    -----
+    Must be overriden. Child classes need to implement:
+
+    glsl_map : string
+        The GLSL function for the colormap. Use $color_0 to refer
+        to the first color in `colors`, and so on. These are vec4 vectors.
+    map(item) : function
+        Takes a (N, 1) vector of values in [0, 1], and returns a rgba array
+        of size (N, 4).
+
+    """
+
+    # Control colors used by the colormap.
+    colors = None
+
+    # GLSL string with a function implementing the color map.
+    glsl_map = None
+
+    def __init__(self, colors=None):
+        # Ensure the colors are arrays.
+        if colors is not None:
+            self.colors = colors
+        if not isinstance(self.colors, ColorArray):
+            self.colors = ColorArray(self.colors)
+        # Process the GLSL map function by replacing $color_i by the
+        if len(self.colors) > 0:
+            self.glsl_map = _process_glsl_template(self.glsl_map,
+                                                   self.colors.rgba)
+
+    def map(self, item):
+        """Return a rgba array for the requested items.
+
+        This function must be overriden by child classes.
+
+        This function doesn't need to implement argument checking on `item`.
+        It can always assume that `item` is a (N, 1) array of values between
+        0 and 1.
+
+        Parameters
+        ----------
+        item : ndarray
+            An array of values in [0,1].
+
+        Returns
+        -------
+        rgba : ndarray
+            An array with rgba values, with one color per item. The shape
+            should be ``item.shape + (4,)``.
+
+        Notes
+        -----
+        Users are expected to use a colormap with ``__getitem__()`` rather
+        than ``map()`` (which implements a lower-level API).
+
+        """
+        raise NotImplementedError()
+
+    def __getitem__(self, item):
+        if isinstance(item, tuple):
+            raise ValueError('ColorArray indexing is only allowed along '
+                             'the first dimension.')
+        # Ensure item is either a scalar or a column vector.
+        item = _vector(item, type='column')
+        # Clip the values in [0, 1].
+        item = np.clip(item, 0., 1.)
+        colors = self.map(item)
+        return ColorArray(colors)
+
+    def __setitem__(self, item, value):
+        raise RuntimeError("It is not possible to set items to "
+                           "BaseColormap instances.")
+
+    def _repr_html_(self):
+        n = 100
+        html = ("""
+                <style>
+                    table.vispy_colormap {
+                        height: 30px;
+                        border: 0;
+                        margin: 0;
+                        padding: 0;
+                    }
+
+                    table.vispy_colormap td {
+                        width: 3px;
+                        border: 0;
+                        margin: 0;
+                        padding: 0;
+                    }
+                </style>
+                <table class="vispy_colormap">
+                """ +
+                '\n'.join([(("""<td style="background-color: %s;"
+                                 title="%s"></td>""") % (color, color))
+                           for color in self[np.linspace(0., 1., n)].hex]) +
+                """
+                </table>
+                """)
+        return html
+
+
+def _default_controls(ncolors):
+    """Generate linearly spaced control points from a set of colors."""
+    return np.linspace(0., 1., ncolors)
+
+
+# List the parameters of every supported interpolation mode.
+_interpolation_info = {
+    'linear': {
+        'ncontrols': lambda ncolors: ncolors,  # take ncolors as argument
+        'glsl_map': _glsl_mix,  # take 'controls' as argument
+        'map': mix,
+    },
+    'zero': {
+        'ncontrols': lambda ncolors: (ncolors+1),
+        'glsl_map': _glsl_step,
+        'map': step,
+    }
+}
+
+
+class Colormap(BaseColormap):
+    """A colormap defining several control colors and an interpolation scheme.
+
+    Parameters
+    ----------
+    colors : list of colors | ColorArray
+        The list of control colors. If not a ``ColorArray``, a new
+        ``ColorArray`` instance is created from this list. See the
+        documentation of ``ColorArray``.
+    controls : array-like
+        The list of control points for the given colors. It should be
+        an increasing list of floating-point number between 0.0 and 1.0.
+        The first control point must be 0.0. The last control point must be
+        1.0. The number of control points depends on the interpolation scheme.
+    interpolation : str
+        The interpolation mode of the colormap. Default: 'linear'. Can also
+        be 'zero'.
+        If 'linear', ncontrols = ncolors (one color per control point).
+        If 'zero', ncontrols = ncolors+1 (one color per bin).
+
+    Examples
+    --------
+    Here is a basic example:
+
+        >>> from vispy.color import Colormap
+        >>> cm = Colormap(['r', 'g', 'b'])
+        >>> cm[0.], cm[0.5], cm[np.linspace(0., 1., 100)]
+
+    """
+    def __init__(self, colors, controls=None, interpolation='linear'):
+        self.interpolation = interpolation
+        ncontrols = self._ncontrols(len(colors))
+        # Default controls.
+        if controls is None:
+            controls = _default_controls(ncontrols)
+        assert len(controls) == ncontrols
+        self._controls = np.array(controls, dtype=np.float32)
+        self.glsl_map = self._glsl_map_generator(self._controls)
+        super(Colormap, self).__init__(colors)
+
+    @property
+    def interpolation(self):
+        """The interpolation mode of the colormap"""
+        return self._interpolation
+
+    @interpolation.setter
+    def interpolation(self, val):
+        if val not in _interpolation_info:
+            raise ValueError('The interpolation mode can only be one of: ' +
+                             ', '.join(sorted(_interpolation_info.keys())))
+        # Get the information of the interpolation mode.
+        info = _interpolation_info[val]
+        # Get the function that generates the GLSL map, as a function of the
+        # controls array.
+        self._glsl_map_generator = info['glsl_map']
+        # Number of controls as a function of the number of colors.
+        self._ncontrols = info['ncontrols']
+        # Python map function.
+        self._map_function = info['map']
+        self._interpolation = val
+
+    def map(self, x):
+        """The Python mapping function from the [0,1] interval to a
+        list of rgba colors
+
+        Parameters
+        ----------
+        x : array-like
+            The values to map.
+
+        Returns
+        -------
+        colors : list
+            List of rgba colors.
+        """
+        return self._map_function(self.colors.rgba, x, self._controls)
+
+
+class CubeHelixColormap(Colormap):
+    def __init__(self, start=0.5, rot=1, gamma=1.0, reverse=True, nlev=32,
+                 minSat=1.2, maxSat=1.2, minLight=0., maxLight=1., **kwargs):
+        """Cube helix colormap
+
+        A full implementation of Dave Green's "cubehelix" for Matplotlib.
+        Based on the FORTRAN 77 code provided in
+        D.A. Green, 2011, BASI, 39, 289.
+
+        http://adsabs.harvard.edu/abs/2011arXiv1108.5083G
+
+        User can adjust all parameters of the cubehelix algorithm.
+        This enables much greater flexibility in choosing color maps, while
+        always ensuring the color map scales in intensity from black
+        to white. A few simple examples:
+
+        Default color map settings produce the standard "cubehelix".
+
+        Create color map in only blues by setting rot=0 and start=0.
+
+        Create reverse (white to black) backwards through the rainbow once
+        by setting rot=1 and reverse=True.
+
+        Parameters
+        ----------
+        start : scalar, optional
+            Sets the starting position in the color space. 0=blue, 1=red,
+            2=green. Defaults to 0.5.
+        rot : scalar, optional
+            The number of rotations through the rainbow. Can be positive
+            or negative, indicating direction of rainbow. Negative values
+            correspond to Blue->Red direction. Defaults to -1.5
+        gamma : scalar, optional
+            The gamma correction for intensity. Defaults to 1.0
+        reverse : boolean, optional
+            Set to True to reverse the color map. Will go from black to
+            white. Good for density plots where shade~density. Defaults to
+            False
+        nlev : scalar, optional
+            Defines the number of discrete levels to render colors at.
+            Defaults to 32.
+        sat : scalar, optional
+            The saturation intensity factor. Defaults to 1.2
+            NOTE: this was formerly known as "hue" parameter
+        minSat : scalar, optional
+            Sets the minimum-level saturation. Defaults to 1.2
+        maxSat : scalar, optional
+            Sets the maximum-level saturation. Defaults to 1.2
+        startHue : scalar, optional
+            Sets the starting color, ranging from [0, 360], as in
+            D3 version by @mbostock
+            NOTE: overrides values in start parameter
+        endHue : scalar, optional
+            Sets the ending color, ranging from [0, 360], as in
+            D3 version by @mbostock
+            NOTE: overrides values in rot parameter
+        minLight : scalar, optional
+            Sets the minimum lightness value. Defaults to 0.
+        maxLight : scalar, optional
+            Sets the maximum lightness value. Defaults to 1.
+        """
+        super(CubeHelixColormap, self).__init__(
+            cubehelix(start=start, rot=rot, gamma=gamma, reverse=reverse,
+                      nlev=nlev, minSat=minSat, maxSat=maxSat,
+                      minLight=minLight, maxLight=maxLight, **kwargs))
+
+
+class _Fire(BaseColormap):
+    colors = [(1.0, 1.0, 1.0, 1.0),
+              (1.0, 1.0, 0.0, 1.0),
+              (1.0, 0.0, 0.0, 1.0)]
+
+    glsl_map = """
+    vec4 fire(float t) {
+        return mix(mix($color_0, $color_1, t),
+                   mix($color_1, $color_2, t*t), t);
+    }
+    """
+
+    def map(self, t):
+        a, b, d = self.colors.rgba
+        c = _mix_simple(a, b, t)
+        e = _mix_simple(b, d, t**2)
+        return _mix_simple(c, e, t)
+
+
+class _Grays(BaseColormap):
+    glsl_map = """
+    vec4 grays(float t) {
+        return vec4(t, t, t, 1.0);
+    }
+    """
+
+    def map(self, t):
+        if isinstance(t, np.ndarray):
+            return np.hstack([t, t, t, np.ones(t.shape)]).astype(np.float32)
+        else:
+            return np.array([t, t, t, 1.0], dtype=np.float32)
+
+
+class _Ice(BaseColormap):
+    glsl_map = """
+    vec4 ice(float t) {
+        return vec4(t, t, 1.0, 1.0);
+    }
+    """
+
+    def map(self, t):
+        if isinstance(t, np.ndarray):
+            return np.hstack([t, t, np.ones(t.shape),
+                              np.ones(t.shape)]).astype(np.float32)
+        else:
+            return np.array([t, t, 1.0, 1.0], dtype=np.float32)
+
+
+class _Hot(BaseColormap):
+    colors = [(0., .33, .66, 1.0),
+              (.33, .66, 1., 1.0)]
+
+    glsl_map = """
+    vec4 hot(float t) {
+        return vec4(smoothstep($color_0.rgb, $color_1.rgb, vec3(t, t, t)),
+                    1.0);
+    }
+    """
+
+    def map(self, t):
+        rgba = self.colors.rgba
+        smoothed = smoothstep(rgba[0, :3], rgba[1, :3], t)
+        return np.hstack((smoothed, np.ones((len(t), 1))))
+
+
+class _Winter(BaseColormap):
+    colors = [(0.0, 0.0, 1.0, 1.0),
+              (0.0, 1.0, 0.5, 1.0)]
+
+    glsl_map = """
+    vec4 winter(float t) {
+        return mix($color_0, $color_1, sqrt(t));
+    }
+    """
+
+    def map(self, t):
+        return _mix_simple(self.colors.rgba[0],
+                           self.colors.rgba[1],
+                           np.sqrt(t))
+
+
+_colormaps = dict(
+    autumn=Colormap([(1., 0., 0., 1.), (1., 1., 0., 1.)]),
+    blues=Colormap([(1., 1., 1., 1.), (0., 0., 1., 1.)]),
+    cool=Colormap([(0., 1., 1., 1.), (1., 0., 1., 1.)]),
+    greens=Colormap([(1., 1., 1., 1.), (0., 1., 0., 1.)]),
+    reds=Colormap([(1., 1., 1., 1.), (1., 0., 0., 1.)]),
+    spring=Colormap([(1., 0., 1., 1.), (1., 1., 0., 1.)]),
+    summer=Colormap([(0., .5, .4, 1.), (1., 1., .4, 1.)]),
+    fire=_Fire(),
+    grays=_Grays(),
+    hot=_Hot(),
+    ice=_Ice(),
+    winter=_Winter(),
+    cubehelix=CubeHelixColormap(),
+)
+
+
+def get_colormap(name):
+    """Obtain a colormap
+
+    Parameters
+    ----------
+    name : str | Colormap
+        Colormap name. Can also be a Colormap for pass-through.
+    """
+    if isinstance(name, BaseColormap):
+        cmap = name
+    else:
+        if not isinstance(name, string_types):
+            raise TypeError('colormap must be a Colormap or string name')
+        if name not in _colormaps:
+            raise KeyError('colormap name %s not found' % name)
+        cmap = _colormaps[name]
+    return cmap
+
+
+def get_colormaps():
+    """Return the list of colormap names."""
+    return _colormaps.copy()
diff --git a/vispy/color/tests/test_color.py b/vispy/color/tests/test_color.py
index 1297a6b..11cb708 100644
--- a/vispy/color/tests/test_color.py
+++ b/vispy/color/tests/test_color.py
@@ -1,13 +1,17 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
-from nose.tools import assert_equal, assert_raises, assert_true
 from numpy.testing import assert_array_equal, assert_allclose
 
-from vispy.color import Color, ColorArray, LinearGradient, get_color_names
+from vispy.color import (Color, ColorArray, get_color_names,
+                         Colormap,
+                         get_color_dict, get_colormap, get_colormaps)
+from vispy.visuals.shaders import Function
 from vispy.util import use_log_level
+from vispy.testing import (run_tests_if_main, assert_equal, assert_raises,
+                           assert_true)
 
 
 def test_color():
@@ -47,6 +51,16 @@ def test_color_array():
     assert_array_equal(x.rgba, .5 * np.ones((3, 4)))
     assert_raises(ValueError, x.__setitem__, (0, 1), 0)
 
+    # test hsv color space colors
+    x = ColorArray(color_space="hsv", color=[(0, 0, 1),
+                   (0, 0, 0.5), (0, 0, 0)])
+    assert_array_equal(x.rgba[0], [1, 1, 1, 1])
+    assert_array_equal(x.rgba[1], [0.5, 0.5, 0.5, 1])
+    assert_array_equal(x.rgba[2], [0, 0, 0, 1])
+
+    x = ColorArray(color_space="hsv")
+    assert_array_equal(x.rgba[0], [0, 0, 0, 1])
+
 
 def test_color_interpretation():
     """Test basic color interpretation API"""
@@ -118,17 +132,21 @@ def test_color_interpretation():
     assert_raises(ValueError, ColorArray, '#ffii00')  # non-hex
     assert_raises(ValueError, ColorArray, '#ff000')  # too short
     assert_raises(ValueError, ColorArray, [0, 0])  # not enough vals
-    with use_log_level('warning', record=True, print_msg=False) as w:
-        c = ColorArray([2., 0., 0.])  # val > 1
-        assert_true(np.all(c.rgb <= 1))
-        c = ColorArray([-1., 0., 0.])  # val < 0
-        assert_true(np.all(c.rgb >= 0))
-        assert_equal(len(w), 2)  # caught warnings
+    assert_raises(ValueError, ColorArray, [2, 0, 0])  # val > 1
+    assert_raises(ValueError, ColorArray, [-1, 0, 0])  # val < 0
+    c = ColorArray([2., 0., 0.], clip=True)  # val > 1
+    assert_true(np.all(c.rgb <= 1))
+    c = ColorArray([-1., 0., 0.], clip=True)  # val < 0
+    assert_true(np.all(c.rgb >= 0))
+
     # make sure our color dict works
     for key in get_color_names():
         assert_true(ColorArray(key))
     assert_raises(ValueError, ColorArray, 'foo')  # unknown color error
 
+    _color_dict = get_color_dict()
+    assert isinstance(_color_dict, dict)
+    assert set(_color_dict.keys()) == set(get_color_names())
 
 # Taken from known values
 hsv_dict = dict(red=(0, 1, 1),
@@ -189,12 +207,143 @@ def test_color_conversion():
         assert_allclose(c.rgb, rgb, atol=1e-4, rtol=1e-4)
 
 
-def test_linear_gradient():
-    """Test basic support for linear gradients"""
-    colors = ['r', 'g', 'b']
-    xs = [0, 1, 2]
-    grad = LinearGradient(ColorArray(colors), xs)
-    colors.extend([[0.5, 0.5, 0], [0, 0, 1], [1, 0, 0]])
-    xs.extend([0.5, 10, -10])
-    for x, c in zip(xs, colors):
-        assert_array_equal(grad[x], ColorArray(c).rgba[0])
+def test_colormap_interpolation():
+    """Test interpolation routines for colormaps."""
+    import vispy.color.colormap as c
+    assert_raises(AssertionError, c._glsl_step, [0., 1.],)
+
+    c._glsl_mix(controls=[0., 1.])
+    c._glsl_mix(controls=[0., .25, 1.])
+
+    for fun in (c._glsl_step, c._glsl_mix):
+        assert_raises(AssertionError, fun, controls=[0.1, 1.],)
+        assert_raises(AssertionError, fun, controls=[0., .9],)
+        assert_raises(AssertionError, fun, controls=[0.1, .9],)
+
+    # Interpolation tests.
+    color_0 = np.array([1., 0., 0.])
+    color_1 = np.array([0., 1., 0.])
+    color_2 = np.array([0., 0., 1.])
+
+    colors_00 = np.vstack((color_0, color_0))
+    colors_01 = np.vstack((color_0, color_1))
+    colors_11 = np.vstack((color_1, color_1))
+    # colors_012 = np.vstack((color_0, color_1, color_2))
+    colors_021 = np.vstack((color_0, color_2, color_1))
+
+    controls_2 = np.array([0., 1.])
+    controls_3 = np.array([0., .25, 1.])
+    x = np.array([-1., 0., 0.1, 0.4, 0.5, 0.6, 1., 2.])[:, None]
+
+    mixed_2 = c.mix(colors_01, x, controls_2)
+    mixed_3 = c.mix(colors_021, x, controls_3)
+
+    for y in mixed_2, mixed_3:
+        assert_allclose(y[:2, :], colors_00)
+        assert_allclose(y[-2:, :], colors_11)
+
+    assert_allclose(mixed_2[:, -1], np.zeros(len(y)))
+
+
+def test_colormap_gradient():
+    """Test gradient colormaps."""
+    cm = Colormap(['r', 'g'])
+    assert_allclose(cm[-1].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[0.].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[0.5].rgba, [[.5, .5, 0, 1]])
+    assert_allclose(cm[1.].rgba, [[0, 1, 0, 1]])
+
+    cm = Colormap(['r', 'g', 'b'])
+    assert_allclose(cm[-1].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[0.].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[.5].rgba, [[0, 1, 0, 1]])
+    assert_allclose(cm[1].rgba, [[0, 0, 1, 1]])
+    assert_allclose(cm[2].rgba, [[0, 0, 1, 1]])
+
+    cm = Colormap(['r', 'g', 'b'], [0., 0.1, 1.0])
+    assert_allclose(cm[-1].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[0.].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[.1].rgba, [[0, 1, 0, 1]])
+    assert_allclose(cm[1].rgba, [[0, 0, 1, 1]], 1e-6, 1e-6)
+    assert_allclose(cm[2].rgba, [[0, 0, 1, 1]], 1e-6, 1e-6)
+
+
+def test_colormap_discrete():
+    """Test discrete colormaps."""
+    cm = Colormap(['r', 'g'], interpolation='zero')
+    assert_allclose(cm[-1].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[0.].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[0.49].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[0.51].rgba, [[0, 1, 0, 1]])
+    assert_allclose(cm[1.].rgba, [[0, 1, 0, 1]])
+
+    cm = Colormap(['r', 'g', 'b'], interpolation='zero')
+    assert_allclose(cm[-1].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[0.].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[.32].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[.34].rgba, [[0, 1, 0, 1]])
+    assert_allclose(cm[.66].rgba, [[0, 1, 0, 1]])
+    assert_allclose(cm[.67].rgba, [[0, 0, 1, 1]])
+    assert_allclose(cm[.99].rgba, [[0, 0, 1, 1]])
+    assert_allclose(cm[1].rgba, [[0, 0, 1, 1]])
+    assert_allclose(cm[1.1].rgba, [[0, 0, 1, 1]])
+
+    cm = Colormap(['r', 'g', 'b'], [0., 0.1, 0.8, 1.0],
+                  interpolation='zero')
+    assert_allclose(cm[-1].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[0.].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[.099].rgba, [[1, 0, 0, 1]])
+    assert_allclose(cm[.101].rgba, [[0, 1, 0, 1]])
+    assert_allclose(cm[.799].rgba, [[0, 1, 0, 1]])
+    assert_allclose(cm[.801].rgba, [[0, 0, 1, 1]])
+    assert_allclose(cm[1].rgba, [[0, 0, 1, 1]], 1e-6, 1e-6)
+    assert_allclose(cm[2].rgba, [[0, 0, 1, 1]], 1e-6, 1e-6)
+
+
+def test_colormap():
+    """Test named colormaps."""
+    autumn = get_colormap('autumn')
+    assert autumn.glsl_map is not ""
+    assert len(autumn[0.]) == 1
+    assert len(autumn[0.5]) == 1
+    assert len(autumn[1.]) == 1
+    assert len(autumn[[0., 0.5, 1.]]) == 3
+    assert len(autumn[np.array([0., 0.5, 1.])]) == 3
+
+    fire = get_colormap('fire')
+    assert_array_equal(fire[0].rgba, np.ones((1, 4)))
+    assert_array_equal(fire[1].rgba, np.array([[1, 0, 0, 1]]))
+
+    grays = get_colormap('grays')
+    assert_array_equal(grays[.5].rgb, np.ones((1, 3)) * .5)
+
+    hot = get_colormap('hot')
+    assert_allclose(hot[0].rgba, [[0, 0, 0, 1]], 1e-6, 1e-6)
+    assert_allclose(hot[0.5].rgba, [[1, .52272022, 0, 1]], 1e-6, 1e-6)
+    assert_allclose(hot[1.].rgba, [[1, 1, 1, 1]], 1e-6, 1e-6)
+
+    # Test the GLSL and Python mapping.
+    for name in get_colormaps():
+        colormap = get_colormap(name)
+        Function(colormap.glsl_map)
+        colors = colormap[np.linspace(-2., 2., 50)]
+        assert colors.rgba.min() >= 0
+        assert colors.rgba.max() <= 1
+
+
+def test_normalize():
+    """Test the _normalize() function."""
+    from vispy.color.colormap import _normalize
+    for x in (-1, 0., .5, 1., 10., 20):
+        assert _normalize(x) == .5
+    assert_allclose(_normalize((-1., 0., 1.)), (0., .5, 1.))
+    assert_allclose(_normalize((-1., 0., 1.), 0., 1.),
+                    (0., 0., 1.))
+    assert_allclose(_normalize((-1., 0., 1.), 0., 1., clip=False),
+                    (-1., 0., 1.))
+
+    y = _normalize(np.random.randn(100, 5), -10., 10.)
+    assert_allclose([y.min(), y.max()], [0.2975, 1-0.2975], 1e-1, 1e-1)
+
+
+run_tests_if_main()
diff --git a/vispy/ext/cocoapy.py b/vispy/ext/cocoapy.py
index dbf662c..98f18b8 100644
--- a/vispy/ext/cocoapy.py
+++ b/vispy/ext/cocoapy.py
@@ -10,7 +10,12 @@ from ctypes import (cdll, util, Structure, cast, byref, POINTER, CFUNCTYPE,
 
 import platform
 import struct
+import sys
 
+if sys.version_info[0] >= 3:
+    string_types = str,
+else:
+    string_types = basestring,  # noqa
 
 # Based on Pyglet code
 
@@ -378,7 +383,7 @@ def should_use_fpret(restype):
 
 
 def send_message(receiver, selName, *args, **kwargs):
-    if isinstance(receiver, str):
+    if isinstance(receiver, string_types):
         receiver = get_class(receiver)
     selector = get_selector(selName)
     restype = kwargs.get('restype', c_void_p)
@@ -506,7 +511,7 @@ def cfunctype_for_encoding(encoding):
 
 
 def create_subclass(superclass, name):
-    if isinstance(superclass, str):
+    if isinstance(superclass, string_types):
         superclass = get_class(superclass)
     return c_void_p(objc.objc_allocateClassPair(superclass,
                                                 ensure_bytes(name), 0))
@@ -657,7 +662,7 @@ class ObjCClass(object):
     _registered_classes = {}
 
     def __new__(cls, class_name_or_ptr):
-        if isinstance(class_name_or_ptr, str):
+        if isinstance(class_name_or_ptr, string_types):
             name = class_name_or_ptr
             ptr = get_class(name)
         else:
@@ -923,7 +928,7 @@ CFAllocatorRef = c_void_p
 CFStringEncoding = c_uint32
 
 cf.CFStringCreateWithCString.restype = c_void_p
-cf.CFStringCreateWithCString.argtypes = [CFAllocatorRef, c_char_p,
+cf.CFStringCreateWithCString.argtypes = [CFAllocatorRef, c_void_p,
                                          CFStringEncoding]
 
 cf.CFRelease.restype = c_void_p
@@ -967,7 +972,7 @@ def cfstring_to_string(cfstring):
     result = cf.CFStringGetCString(cfstring, buffer, len(buffer),
                                    kCFStringEncodingUTF8)
     if result:
-        return buffer.value.decode('utf-8')
+        return buffer.value.decode('utf8')
 
 
 cf.CFDataCreate.restype = c_void_p
@@ -1417,6 +1422,12 @@ quartz.CGDataProviderCreateWithURL.argtypes = [c_void_p]
 quartz.CGFontCreateWithDataProvider.restype = c_void_p
 quartz.CGFontCreateWithDataProvider.argtypes = [c_void_p]
 
+quartz.CGDisplayScreenSize.argtypes = [CGDirectDisplayID]
+quartz.CGDisplayScreenSize.restype = CGSize
+
+quartz.CGDisplayBounds.argtypes = [CGDirectDisplayID]
+quartz.CGDisplayBounds.restype = CGRect
+
 ######################################################################
 
 # CORETEXT
diff --git a/vispy/ext/cubehelix.py b/vispy/ext/cubehelix.py
new file mode 100644
index 0000000..9e0692c
--- /dev/null
+++ b/vispy/ext/cubehelix.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+"""Modified from:
+
+https://raw.githubusercontent.com/jradavenport/cubehelix/master/cubehelix.py
+
+Copyright (c) 2014, James R. A. Davenport and contributors All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+"""
+
+from math import pi
+import numpy as np
+
+
+def cubehelix(start=0.5, rot=1, gamma=1.0, reverse=True, nlev=256.,
+         minSat=1.2, maxSat=1.2, minLight=0., maxLight=1.,
+         **kwargs):
+    """
+    A full implementation of Dave Green's "cubehelix" for Matplotlib.
+    Based on the FORTRAN 77 code provided in
+    D.A. Green, 2011, BASI, 39, 289.
+
+    http://adsabs.harvard.edu/abs/2011arXiv1108.5083G
+
+    User can adjust all parameters of the cubehelix algorithm.
+    This enables much greater flexibility in choosing color maps, while
+    always ensuring the color map scales in intensity from black
+    to white. A few simple examples:
+
+    Default color map settings produce the standard "cubehelix".
+
+    Create color map in only blues by setting rot=0 and start=0.
+
+    Create reverse (white to black) backwards through the rainbow once
+    by setting rot=1 and reverse=True.
+
+    Parameters
+    ----------
+    start : scalar, optional
+        Sets the starting position in the color space. 0=blue, 1=red,
+        2=green. Defaults to 0.5.
+    rot : scalar, optional
+        The number of rotations through the rainbow. Can be positive
+        or negative, indicating direction of rainbow. Negative values
+        correspond to Blue->Red direction. Defaults to -1.5
+    gamma : scalar, optional
+        The gamma correction for intensity. Defaults to 1.0
+    reverse : boolean, optional
+        Set to True to reverse the color map. Will go from black to
+        white. Good for density plots where shade~density. Defaults to False
+    nlev : scalar, optional
+        Defines the number of discrete levels to render colors at.
+        Defaults to 256.
+    sat : scalar, optional
+        The saturation intensity factor. Defaults to 1.2
+        NOTE: this was formerly known as "hue" parameter
+    minSat : scalar, optional
+        Sets the minimum-level saturation. Defaults to 1.2
+    maxSat : scalar, optional
+        Sets the maximum-level saturation. Defaults to 1.2
+    startHue : scalar, optional
+        Sets the starting color, ranging from [0, 360], as in
+        D3 version by @mbostock
+        NOTE: overrides values in start parameter
+    endHue : scalar, optional
+        Sets the ending color, ranging from [0, 360], as in
+        D3 version by @mbostock
+        NOTE: overrides values in rot parameter
+    minLight : scalar, optional
+        Sets the minimum lightness value. Defaults to 0.
+    maxLight : scalar, optional
+        Sets the maximum lightness value. Defaults to 1.
+
+    Returns
+    -------
+    data : ndarray, shape (N, 3)
+        Control points.
+    """
+
+# override start and rot if startHue and endHue are set
+    if kwargs is not None:
+        if 'startHue' in kwargs:
+            start = (kwargs.get('startHue') / 360. - 1.) * 3.
+        if 'endHue' in kwargs:
+            rot = kwargs.get('endHue') / 360. - start / 3. - 1.
+        if 'sat' in kwargs:
+            minSat = kwargs.get('sat')
+            maxSat = kwargs.get('sat')
+
+# set up the parameters
+    fract = np.linspace(minLight, maxLight, nlev)
+    angle = 2.0 * pi * (start / 3.0 + rot * fract + 1.)
+    fract = fract**gamma
+
+    satar = np.linspace(minSat, maxSat, nlev)
+    amp = satar * fract * (1. - fract) / 2.
+
+# compute the RGB vectors according to main equations
+    red = fract + amp * (-0.14861 * np.cos(angle) + 1.78277 * np.sin(angle))
+    grn = fract + amp * (-0.29227 * np.cos(angle) - 0.90649 * np.sin(angle))
+    blu = fract + amp * (1.97294 * np.cos(angle))
+
+# find where RBB are outside the range [0,1], clip
+    red[np.where((red > 1.))] = 1.
+    grn[np.where((grn > 1.))] = 1.
+    blu[np.where((blu > 1.))] = 1.
+
+    red[np.where((red < 0.))] = 0.
+    grn[np.where((grn < 0.))] = 0.
+    blu[np.where((blu < 0.))] = 0.
+
+# optional color reverse
+    if reverse is True:
+        red = red[::-1]
+        blu = blu[::-1]
+        grn = grn[::-1]
+
+    return np.array((red, blu, grn)).T
diff --git a/vispy/ext/decorator.py b/vispy/ext/decorator.py
new file mode 100644
index 0000000..fa79521
--- /dev/null
+++ b/vispy/ext/decorator.py
@@ -0,0 +1,253 @@
+##########################     LICENCE     ###############################
+
+# Copyright (c) 2005-2012, Michele Simionato
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+
+#   Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+#   Redistributions in bytecode form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in
+#   the documentation and/or other materials provided with the
+#   distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+"""
+Decorator module, see http://pypi.python.org/pypi/decorator
+for the documentation.
+"""
+from __future__ import print_function
+
+__version__ = '3.4.0'
+
+__all__ = ["decorator", "FunctionMaker", "contextmanager"]
+
+
+import sys, re, inspect
+if sys.version >= '3':
+    from inspect import getfullargspec
+    def get_init(cls):
+        return cls.__init__
+else:
+    class getfullargspec(object):
+        "A quick and dirty replacement for getfullargspec for Python 2.X"
+        def __init__(self, f):
+            self.args, self.varargs, self.varkw, self.defaults = \
+                inspect.getargspec(f)
+            self.kwonlyargs = []
+            self.kwonlydefaults = None
+        def __iter__(self):
+            yield self.args
+            yield self.varargs
+            yield self.varkw
+            yield self.defaults
+    def get_init(cls):
+        return cls.__init__.__func__
+
+DEF = re.compile('\s*def\s*([_\w][_\w\d]*)\s*\(')
+
+# basic functionality
+class FunctionMaker(object):
+    """
+    An object with the ability to create functions with a given signature.
+    It has attributes name, doc, module, signature, defaults, dict and
+    methods update and make.
+    """
+    def __init__(self, func=None, name=None, signature=None,
+                 defaults=None, doc=None, module=None, funcdict=None):
+        self.shortsignature = signature
+        if func:
+            # func can be a class or a callable, but not an instance method
+            self.name = func.__name__
+            if self.name == '<lambda>': # small hack for lambda functions
+                self.name = '_lambda_'
+            self.doc = func.__doc__
+            self.module = func.__module__
+            if inspect.isfunction(func):
+                argspec = getfullargspec(func)
+                self.annotations = getattr(func, '__annotations__', {})
+                for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs',
+                          'kwonlydefaults'):
+                    setattr(self, a, getattr(argspec, a))
+                for i, arg in enumerate(self.args):
+                    setattr(self, 'arg%d' % i, arg)
+                if sys.version < '3': # easy way
+                    self.shortsignature = self.signature = \
+                        inspect.formatargspec(
+                        formatvalue=lambda val: "", *argspec)[1:-1]
+                else: # Python 3 way
+                    allargs = list(self.args)
+                    allshortargs = list(self.args)
+                    if self.varargs:
+                        allargs.append('*' + self.varargs)
+                        allshortargs.append('*' + self.varargs)
+                    elif self.kwonlyargs:
+                        allargs.append('*') # single star syntax
+                    for a in self.kwonlyargs:
+                        allargs.append('%s=None' % a)
+                        allshortargs.append('%s=%s' % (a, a))
+                    if self.varkw:
+                        allargs.append('**' + self.varkw)
+                        allshortargs.append('**' + self.varkw)
+                    self.signature = ', '.join(allargs)
+                    self.shortsignature = ', '.join(allshortargs)
+                self.dict = func.__dict__.copy()
+        # func=None happens when decorating a caller
+        if name:
+            self.name = name
+        if signature is not None:
+            self.signature = signature
+        if defaults:
+            self.defaults = defaults
+        if doc:
+            self.doc = doc
+        if module:
+            self.module = module
+        if funcdict:
+            self.dict = funcdict
+        # check existence required attributes
+        assert hasattr(self, 'name')
+        if not hasattr(self, 'signature'):
+            raise TypeError('You are decorating a non function: %s' % func)
+
+    def update(self, func, **kw):
+        "Update the signature of func with the data in self"
+        func.__name__ = self.name
+        func.__doc__ = getattr(self, 'doc', None)
+        func.__dict__ = getattr(self, 'dict', {})
+        func.__defaults__ = getattr(self, 'defaults', ())
+        func.__kwdefaults__ = getattr(self, 'kwonlydefaults', None)
+        func.__annotations__ = getattr(self, 'annotations', None)
+        callermodule = sys._getframe(3).f_globals.get('__name__', '?')
+        func.__module__ = getattr(self, 'module', callermodule)
+        func.__dict__.update(kw)
+
+    def make(self, src_templ, evaldict=None, addsource=False, **attrs):
+        "Make a new function from a given template and update the signature"
+        src = src_templ % vars(self) # expand name and signature
+        evaldict = evaldict or {}
+        mo = DEF.match(src)
+        if mo is None:
+            raise SyntaxError('not a valid function template\n%s' % src)
+        name = mo.group(1) # extract the function name
+        names = set([name] + [arg.strip(' *') for arg in
+                             self.shortsignature.split(',')])
+        for n in names:
+            if n in ('_func_', '_call_'):
+                raise NameError('%s is overridden in\n%s' % (n, src))
+        if not src.endswith('\n'): # add a newline just for safety
+            src += '\n' # this is needed in old versions of Python
+        try:
+            code = compile(src, '<string>', 'single')
+            # print >> sys.stderr, 'Compiling %s' % src
+            exec(code, evaldict)
+        except:
+            print('Error in generated code:', file=sys.stderr)
+            print(src, file=sys.stderr)
+            raise
+        func = evaldict[name]
+        if addsource:
+            attrs['__source__'] = src
+        self.update(func, **attrs)
+        return func
+
+    @classmethod
+    def create(cls, obj, body, evaldict, defaults=None,
+               doc=None, module=None, addsource=True, **attrs):
+        """
+        Create a function from the strings name, signature and body.
+        evaldict is the evaluation dictionary. If addsource is true an attribute
+        __source__ is added to the result. The attributes attrs are added,
+        if any.
+        """
+        if isinstance(obj, str): # "name(signature)"
+            name, rest = obj.strip().split('(', 1)
+            signature = rest[:-1] #strip a right parens
+            func = None
+        else: # a function
+            name = None
+            signature = None
+            func = obj
+        self = cls(func, name, signature, defaults, doc, module)
+        ibody = '\n'.join('    ' + line for line in body.splitlines())
+        return self.make('def %(name)s(%(signature)s):\n' + ibody,
+                        evaldict, addsource, **attrs)
+
+def decorator(caller, func=None):
+    """
+    decorator(caller) converts a caller function into a decorator;
+    decorator(caller, func) decorates a function using a caller.
+    """
+    if func is not None: # returns a decorated function
+        evaldict = func.__globals__.copy()
+        evaldict['_call_'] = caller
+        evaldict['_func_'] = func
+        return FunctionMaker.create(
+            func, "return _call_(_func_, %(shortsignature)s)",
+            evaldict, undecorated=func, __wrapped__=func)
+    else: # returns a decorator
+        if inspect.isclass(caller):
+            name = caller.__name__.lower()
+            callerfunc = get_init(caller)
+            doc = 'decorator(%s) converts functions/generators into ' \
+                'factories of %s objects' % (caller.__name__, caller.__name__)
+            fun = getfullargspec(callerfunc).args[1] # second arg
+        elif inspect.isfunction(caller):
+            name = '_lambda_' if caller.__name__ == '<lambda>' \
+                else caller.__name__
+            callerfunc = caller
+            doc = caller.__doc__
+            fun = getfullargspec(callerfunc).args[0] # first arg
+        else: # assume caller is an object with a __call__ method
+            name = caller.__class__.__name__.lower()
+            callerfunc = caller.__call__.__func__
+            doc = caller.__call__.__doc__
+            fun = getfullargspec(callerfunc).args[1] # second arg
+        evaldict = callerfunc.__globals__.copy()
+        evaldict['_call_'] = caller
+        evaldict['decorator'] = decorator
+        return FunctionMaker.create(
+            '%s(%s)' % (name, fun),
+            'return decorator(_call_, %s)' % fun,
+            evaldict, undecorated=caller, __wrapped__=caller,
+            doc=doc, module=caller.__module__)
+
+######################### contextmanager ########################
+
+def __call__(self, func):
+    'Context manager decorator'
+    return FunctionMaker.create(
+        func, "with _self_: return _func_(%(shortsignature)s)",
+        dict(_self_=self, _func_=func), __wrapped__=func)
+
+try: # Python >= 3.2
+
+    from contextlib import _GeneratorContextManager
+    ContextManager = type(
+        'ContextManager', (_GeneratorContextManager,), dict(__call__=__call__))
+
+except ImportError: # Python >= 2.5
+
+    from contextlib import GeneratorContextManager
+    def __init__(self, f, *a, **k):
+        return GeneratorContextManager.__init__(self, f(*a, **k))
+    ContextManager = type(
+        'ContextManager', (GeneratorContextManager,),
+        dict(__call__=__call__, __init__=__init__))
+
+contextmanager = decorator(ContextManager)
diff --git a/vispy/ext/egl.py b/vispy/ext/egl.py
index 141d7fc..50c231c 100644
--- a/vispy/ext/egl.py
+++ b/vispy/ext/egl.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ A ctypes-based API to EGL.
@@ -208,7 +208,7 @@ EGL_ALPHA_FORMAT_PRE = EGL_VG_ALPHA_FORMAT_PRE
 ## The functions
 
 _lib.eglGetDisplay.argtypes = _c_int,
-_lib.eglGetDisplay.restype = c_void_p
+_lib.eglGetDisplay.restype = _c_int
 _lib.eglInitialize.argtypes = c_void_p, _POINTER(_c_int), _POINTER(_c_int)
 _lib.eglTerminate.argtypes = c_void_p,
 _lib.eglChooseConfig.argtypes = (c_void_p, _POINTER(_c_int),
diff --git a/vispy/ext/fontconfig.py b/vispy/ext/fontconfig.py
index 6033b49..073557a 100644
--- a/vispy/ext/fontconfig.py
+++ b/vispy/ext/fontconfig.py
@@ -114,6 +114,5 @@ def find_font(face, bold, italic):
 def _list_fonts():
     """List system fonts"""
     stdout_, stderr = run_subprocess(['fc-list', ':', 'family'])
-    vals = stdout_.decode('utf-8').strip().split('\n')
-    vals = [v.split(',')[0] for v in vals]
+    vals = [v.split(',')[0] for v in stdout_.strip().splitlines(False)]
     return vals
diff --git a/vispy/ext/freetype.py b/vispy/ext/freetype.py
old mode 100644
new mode 100755
index e766555..af2d6b6
--- a/vispy/ext/freetype.py
+++ b/vispy/ext/freetype.py
@@ -19,7 +19,7 @@ from ctypes import (byref, c_char_p, c_ushort, cast, util, CDLL, Structure,
                     c_char, c_ubyte, CFUNCTYPE)
 
 from .six import string_types
-from ..util.fetching import load_data_file
+from ..util import load_data_file
 
 FT_LOAD_RENDER = 4
 FT_KERNING_DEFAULT = 0
@@ -210,7 +210,7 @@ def get_handle():
         __handle__ = FT_Library()
         error = FT_Init_FreeType(byref(__handle__))
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
     return __handle__
 
 
@@ -333,7 +333,7 @@ class Glyph(object):
         error = FT_Glyph_To_Bitmap(byref(self._FT_Glyph),
                                    mode, origin, destroy)
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
         return BitmapGlyph(self._FT_Glyph)
 
 
@@ -355,7 +355,7 @@ class GlyphSlot(object):
         aglyph = FT_Glyph()
         error = FT_Get_Glyph(self._FT_GlyphSlot, byref(aglyph))
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
         return Glyph(aglyph)
 
     bitmap = property(lambda self: Bitmap(self._FT_GlyphSlot.contents.bitmap))
@@ -377,7 +377,7 @@ class Face(object):
         u_filename = c_char_p(filename.encode('utf-8'))
         error = FT_New_Face(library, u_filename, index, byref(face))
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
         self._filename = filename
         self._index = index
         self._FT_Face = face
@@ -389,27 +389,27 @@ class Face(object):
     def attach_file(self, filename):
         error = FT_Attach_File(self._FT_Face, filename)
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
 
     def set_char_size(self, width=0, height=0, hres=72, vres=72):
         error = FT_Set_Char_Size(self._FT_Face, width, height, hres, vres)
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError('Could not set size: %s' % hex(error))
 
     def set_pixel_sizes(self, width, height):
         error = FT_Set_Pixel_Sizes(self._FT_Face, width, height)
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
 
     def select_charmap(self, encoding):
         error = FT_Select_Charmap(self._FT_Face, encoding)
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
 
     def set_charmap(self, charmap):
         error = FT_Set_Charmap(self._FT_Face, charmap._FT_Charmap)
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
 
     def get_char_index(self, charcode):
         if isinstance(charcode, string_types):
@@ -436,25 +436,25 @@ class Face(object):
     def select_size(self, strike_index):
         error = FT_Select_Size(self._FT_Face, strike_index)
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
 
     def load_glyph(self, index, flags=FT_LOAD_RENDER):
         error = FT_Load_Glyph(self._FT_Face, index, flags)
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
 
     def load_char(self, char, flags=FT_LOAD_RENDER):
         if len(char) == 1:
             char = ord(char)
         error = FT_Load_Char(self._FT_Face, char, flags)
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
 
     def get_advance(self, gindex, flags):
         padvance = FT_Fixed(0)
         error = FT_Get_Advance(self._FT_Face, gindex, flags, byref(padvance))
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
         return padvance.value
 
     def get_kerning(self, left, right, mode=FT_KERNING_DEFAULT):
@@ -464,7 +464,7 @@ class Face(object):
         error = FT_Get_Kerning(self._FT_Face,
                                left_glyph, right_glyph, mode, byref(kerning))
         if error:
-            raise RuntimeError(error)
+            raise RuntimeError(hex(error))
         return kerning
 
     def get_format(self):
diff --git a/vispy/ext/gdi32plus.py b/vispy/ext/gdi32plus.py
index 71433db..7622286 100644
--- a/vispy/ext/gdi32plus.py
+++ b/vispy/ext/gdi32plus.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
@@ -57,6 +57,12 @@ UINT32 = c_uint32
 HDC = c_void_p
 PSTR = c_uint64 if _64_bit else c_uint
 
+HORZSIZE = 4
+VERTSIZE = 6
+
+HORZRES = 8
+VERTRES = 10
+
 
 # gdi32
 
@@ -145,11 +151,22 @@ gdi32.GetOutlineTextMetricsW.restype = UINT
 gdi32.GetOutlineTextMetricsW.argtypes = [HDC, UINT,
                                          POINTER(OUTLINETEXTMETRIC)]
 
+
+gdi32.GetDeviceCaps.argtypes = [HDC, INT]
+gdi32.GetDeviceCaps.restype = INT
+
 user32 = windll.user32
 
 user32.GetDC.restype = HDC  # HDC
 user32.GetDC.argtypes = [UINT32]  # HWND
 
+user32.ReleaseDC.argtypes = [c_void_p, HDC]
+
+try:
+    user32.SetProcessDPIAware.argtypes = []
+except AttributeError:
+    pass  # not present on XP
+
 
 # gdiplus
 
diff --git a/vispy/ext/glfw.py b/vispy/ext/glfw.py
index a353484..39e0f82 100644
--- a/vispy/ext/glfw.py
+++ b/vispy/ext/glfw.py
@@ -73,7 +73,7 @@ version = glfwGetVersion()
 
 if version[0] != 3:
     version = '.'.join([str(v) for v in version])
-    raise OSError('Need GLFW v3, found %s' % version)
+    raise OSError('Need GLFW library version 3, found version %s' % version)
 
 
 # --- Version -----------------------------------------------------------------
@@ -276,6 +276,8 @@ GLFW_ICONIFIED              = 0x00020002
 GLFW_RESIZABLE              = 0x00020003
 GLFW_VISIBLE                = 0x00020004
 GLFW_DECORATED              = 0x00020005
+GLFW_AUTO_ICONIFY           = 0x00020006
+GLFW_FLOATING               = 0x00020007
 
 # ---
 GLFW_RED_BITS               = 0x00021001
@@ -462,6 +464,7 @@ glfwExtensionSupported         = _glfw.glfwExtensionSupported
 glfwGetProcAddress             = _glfw.glfwGetProcAddress
 
 
+
 # --- Pythonizer --------------------------------------------------------------
 
 # This keeps track of current windows
@@ -471,14 +474,13 @@ __destroyed__ = []
 # This is to prevent garbage collection on callbacks
 __c_callbacks__ = {}
 __py_callbacks__ = {}
-
+__c_error_callback__ = None
 
 def glfwCreateWindow(width=640, height=480, title="GLFW Window",
                      monitor=None, share=None):
     _glfw.glfwCreateWindow.restype = POINTER(GLFWwindow)
     window = _glfw.glfwCreateWindow(int(width), int(height),
-                                    title.encode('ASCII'), monitor, share)
-    assert window not in __windows__
+                                    title.encode('utf-8'), monitor, share)
     __windows__.append(window)
     __destroyed__.append(False)
     index = __windows__.index(window)
@@ -504,12 +506,12 @@ def glfwCreateWindow(width=640, height=480, title="GLFW Window",
 def glfwDestroyWindow(window):
     index = __windows__.index(window)
     if not __destroyed__[index]:
-        __destroyed__[index] = True
         # We do not delete window from the list (or it would impact numbering)
         __windows__[index] = None
         _glfw.glfwDestroyWindow(window)
         del __c_callbacks__[index]
         del __py_callbacks__[index]
+    __destroyed__[index] = True
 
 
 def glfwGetWindowPos(window):
@@ -572,13 +574,13 @@ def glfwGetMonitorPhysicalSize(monitor):
 
 def glfwGetVideoMode(monitor):
     _glfw.glfwGetVideoMode.restype = POINTER(GLFWvidmode)
-    c_modes = _glfw.glfwGetVideoModes(monitor)
-    return (c_modes.width,
-            c_modes.height,
-            c_modes.redBits,
-            c_modes.blueBits,
-            c_modes.greenBits,
-            c_modes.refreshRate )
+    c_mode = _glfw.glfwGetVideoMode(monitor).contents
+    return (c_mode.width,
+            c_mode.height,
+            c_mode.redBits,
+            c_mode.blueBits,
+            c_mode.greenBits,
+            c_mode.refreshRate )
 
 
 def GetGammaRamp(monitor):
@@ -625,7 +627,6 @@ def %(callback)s(window, callback = None):
     return old_callback""" % {'callback': callback, 'fun': fun}
     return code
 
-exec(__callback__('Error'))
 exec(__callback__('Monitor'))
 exec(__callback__('WindowPos'))
 exec(__callback__('WindowSize'))
@@ -639,3 +640,10 @@ exec(__callback__('Char'))
 exec(__callback__('MouseButton'))
 exec(__callback__('CursorPos'))
 exec(__callback__('Scroll'))
+
+
+# Error callback does not take window parameter
+def glfwSetErrorCallback(callback = None):
+    global __c_error_callback__
+    __c_error_callback__ = errorfun(callback)
+    _glfw.glfwSetErrorCallback(__c_error_callback__)
\ No newline at end of file
diff --git a/vispy/ext/gzip_open.py b/vispy/ext/gzip_open.py
index 7002f4b..b7d4d52 100644
--- a/vispy/ext/gzip_open.py
+++ b/vispy/ext/gzip_open.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 from gzip import GzipFile
 
diff --git a/vispy/ext/ipy_inputhook.py b/vispy/ext/ipy_inputhook.py
new file mode 100644
index 0000000..ed35cd1
--- /dev/null
+++ b/vispy/ext/ipy_inputhook.py
@@ -0,0 +1,301 @@
+# coding: utf-8
+"""
+Inputhook management for GUI event loop integration.
+"""
+
+#-----------------------------------------------------------------------------
+# Source:
+#   https://github.com/ipython/ipython/commits/master/IPython/lib/inputhook.py
+# Revision:
+#   29a0deb452d4fa7f59edb7e059c1a46ceb5a124d
+# Modifications:
+#   Removed dependencies and other backends for VisPy.
+#-----------------------------------------------------------------------------
+#  Copyright (C) 2008-2011  The IPython Development Team
+#
+#  Distributed under the terms of the BSD License.  The full license is in
+#  the file COPYING, distributed as part of this software.
+#-----------------------------------------------------------------------------
+
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
+try:
+    import ctypes
+except ImportError:
+    ctypes = None
+except SystemError: # IronPython issue, 2/8/2014
+    ctypes = None
+import os
+import sys
+
+from distutils.version import LooseVersion as V
+
+
+#-----------------------------------------------------------------------------
+# Constants
+#-----------------------------------------------------------------------------
+
+# Constants for identifying the GUI toolkits.
+GUI_WX = 'wx'
+GUI_QT = 'qt'
+GUI_QT4 = 'qt4'
+GUI_GTK = 'gtk'
+GUI_TK = 'tk'
+GUI_OSX = 'osx'
+GUI_PYGLET = 'pyglet'
+GUI_GTK3 = 'gtk3'
+GUI_NONE = 'none' # i.e. disable
+
+#-----------------------------------------------------------------------------
+# Utilities
+#-----------------------------------------------------------------------------
+
+def _stdin_ready_posix():
+    """Return True if there's something to read on stdin (posix version)."""
+    infds, outfds, erfds = select.select([sys.stdin],[],[],0)
+    return bool(infds)
+
+def _stdin_ready_nt():
+    """Return True if there's something to read on stdin (nt version)."""
+    return msvcrt.kbhit()
+
+def _stdin_ready_other():
+    """Return True, assuming there's something to read on stdin."""
+    return True #
+
+
+def _ignore_CTRL_C_posix():
+    """Ignore CTRL+C (SIGINT)."""
+    signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+def _allow_CTRL_C_posix():
+    """Take CTRL+C into account (SIGINT)."""
+    signal.signal(signal.SIGINT, signal.default_int_handler)
+
+def _ignore_CTRL_C_other():
+    """Ignore CTRL+C (not implemented)."""
+    pass
+
+def _allow_CTRL_C_other():
+    """Take CTRL+C into account (not implemented)."""
+    pass
+
+if os.name == 'posix':
+    import select
+    import signal
+    stdin_ready = _stdin_ready_posix
+    ignore_CTRL_C = _ignore_CTRL_C_posix
+    allow_CTRL_C = _allow_CTRL_C_posix
+elif os.name == 'nt':
+    import msvcrt
+    stdin_ready = _stdin_ready_nt
+    ignore_CTRL_C = _ignore_CTRL_C_other
+    allow_CTRL_C = _allow_CTRL_C_other
+else:
+    stdin_ready = _stdin_ready_other
+    ignore_CTRL_C = _ignore_CTRL_C_other
+    allow_CTRL_C = _allow_CTRL_C_other
+
+
+#-----------------------------------------------------------------------------
+# Main InputHookManager class
+#-----------------------------------------------------------------------------
+
+
+class InputHookManager(object):
+    """Manage PyOS_InputHook for different GUI toolkits.
+
+    This class installs various hooks under ``PyOSInputHook`` to handle
+    GUI event loop integration.
+    """
+    
+    def __init__(self):
+        if ctypes is None:
+            print("IPython GUI event loop requires ctypes, %gui will not be available")
+            return
+        self.PYFUNC = ctypes.PYFUNCTYPE(ctypes.c_int)
+        self.guihooks = {}
+        self.aliases = {}
+        self.apps = {}
+        self._reset()
+
+    def _reset(self):
+        self._callback_pyfunctype = None
+        self._callback = None
+        self._installed = False
+        self._current_gui = None
+
+    def get_pyos_inputhook(self):
+        """Return the current PyOS_InputHook as a ctypes.c_void_p."""
+        return ctypes.c_void_p.in_dll(ctypes.pythonapi,"PyOS_InputHook")
+
+    def get_pyos_inputhook_as_func(self):
+        """Return the current PyOS_InputHook as a ctypes.PYFUNCYPE."""
+        return self.PYFUNC.in_dll(ctypes.pythonapi,"PyOS_InputHook")
+
+    def set_inputhook(self, callback):
+        """Set PyOS_InputHook to callback and return the previous one."""
+        # On platforms with 'readline' support, it's all too likely to
+        # have a KeyboardInterrupt signal delivered *even before* an
+        # initial ``try:`` clause in the callback can be executed, so
+        # we need to disable CTRL+C in this situation.
+        ignore_CTRL_C()
+        self._callback = callback
+        self._callback_pyfunctype = self.PYFUNC(callback)
+        pyos_inputhook_ptr = self.get_pyos_inputhook()
+        original = self.get_pyos_inputhook_as_func()
+        pyos_inputhook_ptr.value = \
+            ctypes.cast(self._callback_pyfunctype, ctypes.c_void_p).value
+        self._installed = True
+        return original
+
+    def clear_inputhook(self, app=None):
+        """Set PyOS_InputHook to NULL and return the previous one.
+
+        Parameters
+        ----------
+        app : optional, ignored
+          This parameter is allowed only so that clear_inputhook() can be
+          called with a similar interface as all the ``enable_*`` methods.  But
+          the actual value of the parameter is ignored.  This uniform interface
+          makes it easier to have user-level entry points in the main IPython
+          app like :meth:`enable_gui`."""
+        pyos_inputhook_ptr = self.get_pyos_inputhook()
+        original = self.get_pyos_inputhook_as_func()
+        pyos_inputhook_ptr.value = ctypes.c_void_p(None).value
+        allow_CTRL_C()
+        self._reset()
+        return original
+
+    def clear_app_refs(self, gui=None):
+        """Clear IPython's internal reference to an application instance.
+
+        Whenever we create an app for a user on qt4 or wx, we hold a
+        reference to the app.  This is needed because in some cases bad things
+        can happen if a user doesn't hold a reference themselves.  This
+        method is provided to clear the references we are holding.
+
+        Parameters
+        ----------
+        gui : None or str
+            If None, clear all app references.  If ('wx', 'qt4') clear
+            the app for that toolkit.  References are not held for gtk or tk
+            as those toolkits don't have the notion of an app.
+        """
+        if gui is None:
+            self.apps = {}
+        elif gui in self.apps:
+            del self.apps[gui]
+
+    def register(self, toolkitname, *aliases):
+        """Register a class to provide the event loop for a given GUI.
+        
+        This is intended to be used as a class decorator. It should be passed
+        the names with which to register this GUI integration. The classes
+        themselves should subclass :class:`InputHookBase`.
+        
+        ::
+        
+            @inputhook_manager.register('qt')
+            class QtInputHook(InputHookBase):
+                def enable(self, app=None):
+                    ...
+        """
+        def decorator(cls):
+            inst = cls(self)
+            self.guihooks[toolkitname] = inst
+            for a in aliases:
+                self.aliases[a] = toolkitname
+            return cls
+        return decorator
+
+    def current_gui(self):
+        """Return a string indicating the currently active GUI or None."""
+        return self._current_gui
+
+    def enable_gui(self, gui=None, app=None):
+        """Switch amongst GUI input hooks by name.
+
+        This is a higher level method than :meth:`set_inputhook` - it uses the
+        GUI name to look up a registered object which enables the input hook
+        for that GUI.
+
+        Parameters
+        ----------
+        gui : optional, string or None
+          If None (or 'none'), clears input hook, otherwise it must be one
+          of the recognized GUI names (see ``GUI_*`` constants in module).
+
+        app : optional, existing application object.
+          For toolkits that have the concept of a global app, you can supply an
+          existing one.  If not given, the toolkit will be probed for one, and if
+          none is found, a new one will be created.  Note that GTK does not have
+          this concept, and passing an app if ``gui=="GTK"`` will raise an error.
+
+        Returns
+        -------
+        The output of the underlying gui switch routine, typically the actual
+        PyOS_InputHook wrapper object or the GUI toolkit app created, if there was
+        one.
+        """
+        if gui in (None, GUI_NONE):
+            return self.disable_gui()
+        
+        if gui in self.aliases:
+            return self.enable_gui(self.aliases[gui], app)
+        
+        try:
+            gui_hook = self.guihooks[gui]
+        except KeyError:
+            e = "Invalid GUI request {!r}, valid ones are: {}"
+            raise ValueError(e.format(gui, ', '.join(self.guihooks)))
+        self._current_gui = gui
+
+        app = gui_hook.enable(app)
+        if app is not None:
+            app._in_event_loop = True
+            self.apps[gui] = app        
+        return app
+
+    def disable_gui(self):
+        """Disable GUI event loop integration.
+        
+        If an application was registered, this sets its ``_in_event_loop``
+        attribute to False. It then calls :meth:`clear_inputhook`.
+        """
+        gui = self._current_gui
+        if gui in self.apps:
+            self.apps[gui]._in_event_loop = False
+        return self.clear_inputhook()
+
+class InputHookBase(object):
+    """Base class for input hooks for specific toolkits.
+    
+    Subclasses should define an :meth:`enable` method with one argument, ``app``,
+    which will either be an instance of the toolkit's application class, or None.
+    They may also define a :meth:`disable` method with no arguments.
+    """
+    def __init__(self, manager):
+        self.manager = manager
+
+    def disable(self):
+        pass
+
+inputhook_manager = InputHookManager()
+
+ at inputhook_manager.register('osx')
+class NullInputHook(InputHookBase):
+    """A null inputhook that doesn't need to do anything"""
+    def enable(self, app=None):
+        pass
+
+clear_inputhook = inputhook_manager.clear_inputhook
+set_inputhook = inputhook_manager.set_inputhook
+current_gui = inputhook_manager.current_gui
+clear_app_refs = inputhook_manager.clear_app_refs
+enable_gui = inputhook_manager.enable_gui
+disable_gui = inputhook_manager.disable_gui
+register = inputhook_manager.register
+guis = inputhook_manager.guihooks
\ No newline at end of file
diff --git a/vispy/ext/mplutils.py b/vispy/ext/mplutils.py
index e998af6..00e4868 100644
--- a/vispy/ext/mplutils.py
+++ b/vispy/ext/mplutils.py
@@ -155,8 +155,8 @@ def get_marker_style(line):
     style['marker'] = line.get_marker()
     markerstyle = MarkerStyle(line.get_marker())
     markersize = line.get_markersize()
-    markertransform = (markerstyle.get_transform()
-                       + Affine2D().scale(markersize, -markersize))
+    markertransform = (markerstyle.get_transform() +
+                       Affine2D().scale(markersize, -markersize))
     style['markerpath'] = SVG_path(markerstyle.get_path(),
                                    markertransform)
     style['markersize'] = markersize
diff --git a/vispy/ext/ordereddict.py b/vispy/ext/ordereddict.py
index b41b1b6..90c2e64 100644
--- a/vispy/ext/ordereddict.py
+++ b/vispy/ext/ordereddict.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from sys import version_info
 
-if version_info[0] > 2 or version_info[1] >= 7:
+if version_info >= (2, 7):
     from collections import OrderedDict
 else:
     from .py24_ordereddict import OrderedDict  # noqa
diff --git a/vispy/ext/py24_ordereddict.py b/vispy/ext/py24_ordereddict.py
index b6f4193..5e4412c 100644
--- a/vispy/ext/py24_ordereddict.py
+++ b/vispy/ext/py24_ordereddict.py
@@ -30,7 +30,7 @@ class OrderedDict(dict):
     # The sentinel element never gets deleted (this simplifies the algorithm).
     # Each link is stored as a list of length three:  [PREV, NEXT, KEY].
 
-    def __init__(self, *args, **kwds):
+    def __init__(self, *args, **kwargs):
         '''Initialize an ordered dictionary.  Signature is the same as for
         regular dictionaries, but keyword arguments are not recommended
         because their insertion order is arbitrary.
@@ -44,7 +44,7 @@ class OrderedDict(dict):
             self.__root = root = []                     # sentinel node
             root[:] = [root, root, None]
             self.__map = {}
-        self.__update(*args, **kwds)
+        self.__update(*args, **kwargs)
 
     def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
         'od.__setitem__(i, y) <==> od[i]=y'
@@ -146,7 +146,7 @@ class OrderedDict(dict):
         for k in self:
             yield (k, self[k])
 
-    def update(*args, **kwds):
+    def update(*args, **kwargs):
         '''od.update(E, **F) -> None.  Update od from dict/iterable E and F.
 
         If E is a dict instance, does:           for k in E: od[k] = E[k]
@@ -174,7 +174,7 @@ class OrderedDict(dict):
         else:
             for key, value in other:
                 self[key] = value
-        for key, value in kwds.items():
+        for key, value in kwargs.items():
             self[key] = value
 
     # let subclasses override update without breaking __init__
diff --git a/vispy/ext/six.py b/vispy/ext/six.py
index b3595a4..e29545c 100644
--- a/vispy/ext/six.py
+++ b/vispy/ext/six.py
@@ -20,6 +20,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
+import io
 import operator
 import sys
 import types
@@ -38,6 +39,7 @@ if PY3:
     class_types = type,
     text_type = str
     binary_type = bytes
+    file_types = (io.TextIOWrapper, io.BufferedRandom)
 
     MAXSIZE = sys.maxsize
 else:
@@ -46,6 +48,7 @@ else:
     class_types = (type, types.ClassType)
     text_type = unicode
     binary_type = str
+    file_types = (file, io.TextIOWrapper, io.BufferedRandom)
 
     if sys.platform.startswith("java"):
         # Jython always uses 32 bits.
diff --git a/vispy/geometry/__init__.py b/vispy/geometry/__init__.py
index 6d92c0d..2aa208f 100644
--- a/vispy/geometry/__init__.py
+++ b/vispy/geometry/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -8,12 +8,16 @@ This module implements classes and methods for handling geometric data.
 
 from __future__ import division
 
-__all__ = ['MeshData', 'PolygonData', 'Rect', 'Triangulation', 'create_cube',
-           'create_cylinder', 'create_sphere']
+__all__ = ['MeshData', 'PolygonData', 'Rect', 'Triangulation', 'triangulate',
+           'create_arrow', 'create_cone', 'create_cube', 'create_cylinder',
+           'create_sphere', 'resize']
 
 from .polygon import PolygonData  # noqa
 from .meshdata import MeshData  # noqa
 from .rect import Rect  # noqa
-from .triangulation import Triangulation  # noqa
-from .calculations import _calculate_normals, _fast_cross_3d  # noqa
-from .generation import create_cube, create_cylinder, create_sphere  # noqa
+from .triangulation import Triangulation, triangulate  # noqa
+from .torusknot import TorusKnot  # noqa
+from .calculations import (_calculate_normals, _fast_cross_3d,  # noqa
+                           resize)  # noqa
+from .generation import create_arrow, create_cone, create_cube, \
+           create_cylinder, create_sphere  # noqa
diff --git a/vispy/geometry/_triangulation_debugger.py b/vispy/geometry/_triangulation_debugger.py
index 041ae87..5d43728 100644
--- a/vispy/geometry/_triangulation_debugger.py
+++ b/vispy/geometry/_triangulation_debugger.py
@@ -98,18 +98,18 @@ class DebugTriangulation(Triangulation):
         self.win.removeItem(shape)
         self.draw_state()
         
-    def add_tri(self, *args, **kwds):
-        Triangulation.add_tri(self, *args, **kwds)
+    def add_tri(self, *args, **kwargs):
+        Triangulation._add_tri(self, *args, **kwargs)
         self.draw_tri(list(self.tris.keys())[-1], 
-                      source=kwds.get('source', None))
+                      source=kwargs.get('source', None))
     
-    def remove_tri(self, *args, **kwds):
-        k = Triangulation.remove_tri(self, *args, **kwds)
+    def remove_tri(self, *args, **kwargs):
+        k = Triangulation._remove_tri(self, *args, **kwargs)
         self.undraw_tri(k)
 
-    def edge_event(self, *args, **kwds):
+    def edge_event(self, *args, **kwargs):
         self.draw_state()
-        Triangulation.edge_event(self, *args, **kwds)
+        Triangulation._edge_event(self, *args, **kwargs)
         self.draw_state()
 
 
diff --git a/vispy/geometry/calculations.py b/vispy/geometry/calculations.py
index 590351b..19336c8 100644
--- a/vispy/geometry/calculations.py
+++ b/vispy/geometry/calculations.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """Miscellaneous functions
@@ -7,6 +7,8 @@
 
 import numpy as np
 
+from ..ext.six import string_types
+
 
 ###############################################################################
 # These fast normal calculation routines are adapted from mne-python
@@ -78,3 +80,57 @@ def _calculate_normals(rr, tris):
     size[size == 0] = 1.0  # prevent ugly divide-by-zero
     nn /= size[:, np.newaxis]
     return nn
+
+
+def resize(image, shape, kind='linear'):
+    """Resize an image
+
+    Parameters
+    ----------
+    image : ndarray
+        Array of shape (N, M, ...).
+    shape : tuple
+        2-element shape.
+    kind : str
+        Interpolation, either "linear" or "nearest".
+
+    Returns
+    -------
+    scaled_image : ndarray
+        New image, will have dtype np.float64.
+    """
+    image = np.array(image, float)
+    shape = np.array(shape, int)
+    if shape.ndim != 1 or shape.size != 2:
+        raise ValueError('shape must have two elements')
+    if image.ndim < 2:
+        raise ValueError('image must have two dimensions')
+    if not isinstance(kind, string_types) or kind not in ('nearest', 'linear'):
+        raise ValueError('mode must be "nearest" or "linear"')
+
+    r = np.linspace(0, image.shape[0] - 1, shape[0])
+    c = np.linspace(0, image.shape[1] - 1, shape[1])
+    if kind == 'linear':
+        r_0 = np.floor(r).astype(int)
+        c_0 = np.floor(c).astype(int)
+        r_1 = r_0 + 1
+        c_1 = c_0 + 1
+
+        top = (r_1 - r)[:, np.newaxis]
+        bot = (r - r_0)[:, np.newaxis]
+        lef = (c - c_0)[np.newaxis, :]
+        rig = (c_1 - c)[np.newaxis, :]
+
+        c_1 = np.minimum(c_1, image.shape[1] - 1)
+        r_1 = np.minimum(r_1, image.shape[0] - 1)
+        for arr in (top, bot, lef, rig):
+            arr.shape = arr.shape + (1,) * (image.ndim - 2)
+        out = top * rig * image[r_0][:, c_0, ...]
+        out += bot * rig * image[r_1][:, c_0, ...]
+        out += top * lef * image[r_0][:, c_1, ...]
+        out += bot * lef * image[r_1][:, c_1, ...]
+    else:  # kind == 'nearest'
+        r = np.round(r).astype(int)
+        c = np.round(c).astype(int)
+        out = image[r][:, c, ...]
+    return out
diff --git a/vispy/geometry/generation.py b/vispy/geometry/generation.py
index ae81336..0ec5b94 100644
--- a/vispy/geometry/generation.py
+++ b/vispy/geometry/generation.py
@@ -1,11 +1,13 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Nicolas P .Rougier
 # Date:   04/03/2014
 # -----------------------------------------------------------------------------
+from __future__ import division
+
 import numpy as np
 
 from .meshdata import MeshData
@@ -38,8 +40,8 @@ def create_cube():
                   [-1, 0, 1], [0, -1, 0], [0, 0, -1]])
 
     # Vertice colors
-    c = np.array([[0, 1, 1, 1], [0, 0, 1, 1], [0, 0, 0, 1], [0, 1, 0, 1],
-                  [1, 1, 0, 1], [1, 1, 1, 1], [1, 0, 1, 1], [1, 0, 0, 1]])
+    c = np.array([[1, 1, 1, 1], [0, 1, 1, 1], [0, 0, 1, 1], [1, 0, 1, 1],
+                  [1, 0, 0, 1], [1, 1, 0, 1], [0, 1, 0, 1], [0, 0, 0, 1]])
 
     # Texture coords
     t = np.array([[0, 0], [0, 1], [1, 1], [1, 0]])
@@ -78,6 +80,7 @@ def create_cube():
     filled = np.resize(
         np.array([0, 1, 2, 0, 2, 3], dtype=itype), 6 * (2 * 3))
     filled += np.repeat(4 * np.arange(6, dtype=itype), 6)
+    filled = filled.reshape((len(filled) // 3, 3))
 
     outline = np.resize(
         np.array([0, 1, 1, 2, 2, 3, 3, 0], dtype=itype), 6 * (2 * 4))
@@ -107,7 +110,7 @@ def create_sphere(rows, cols, radius=1.0, offset=True):
     """
     verts = np.empty((rows+1, cols, 3), dtype=np.float32)
 
-    ## compute vertices
+    # compute vertices
     phi = (np.arange(rows+1) * np.pi / rows).reshape(rows+1, 1)
     s = radius * np.sin(phi)
     verts[..., 2] = radius * np.cos(phi)
@@ -117,25 +120,25 @@ def create_sphere(rows, cols, radius=1.0, offset=True):
         th = th + ((np.pi / cols) * np.arange(rows+1).reshape(rows+1, 1))
     verts[..., 0] = s * np.cos(th)
     verts[..., 1] = s * np.sin(th)
-    ## remove redundant vertices from top and bottom
+    # remove redundant vertices from top and bottom
     verts = verts.reshape((rows+1)*cols, 3)[cols-1:-(cols-1)]
 
-    ## compute faces
+    # compute faces
     faces = np.empty((rows*cols*2, 3), dtype=np.uint32)
     rowtemplate1 = (((np.arange(cols).reshape(cols, 1) +
-                      np.array([[1, 0, 0]])) % cols)
-                    + np.array([[0, 0, cols]]))
+                      np.array([[1, 0, 0]])) % cols) +
+                    np.array([[0, 0, cols]]))
     rowtemplate2 = (((np.arange(cols).reshape(cols, 1) +
-                      np.array([[1, 0, 1]])) % cols)
-                    + np.array([[0, cols, cols]]))
+                      np.array([[1, 0, 1]])) % cols) +
+                    np.array([[0, cols, cols]]))
     for row in range(rows):
         start = row * cols * 2
         faces[start:start+cols] = rowtemplate1 + row * cols
         faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols
-    ## cut off zero-area triangles at top and bottom
+    # cut off zero-area triangles at top and bottom
     faces = faces[cols:-cols]
 
-    ## adjust for redundant vertices that were removed from top and bottom
+    # adjust for redundant vertices that were removed from top and bottom
     vmin = cols-1
     faces[faces < vmin] = vmin
     faces -= vmin
@@ -168,7 +171,7 @@ def create_cylinder(rows, cols, radius=[1.0, 1.0], length=1.0, offset=False):
     verts = np.empty((rows+1, cols, 3), dtype=np.float32)
     if isinstance(radius, int):
         radius = [radius, radius]  # convert to list
-    ## compute vertices
+    # compute vertices
     th = np.linspace(2 * np.pi, 0, cols).reshape(1, cols)
     # radius as a function of z
     r = np.linspace(radius[0], radius[1], num=rows+1,
@@ -176,23 +179,114 @@ def create_cylinder(rows, cols, radius=[1.0, 1.0], length=1.0, offset=False):
     verts[..., 2] = np.linspace(0, length, num=rows+1,
                                 endpoint=True).reshape(rows+1, 1)  # z
     if offset:
-        ## rotate each row by 1/2 column
+        # rotate each row by 1/2 column
         th = th + ((np.pi / cols) * np.arange(rows+1).reshape(rows+1, 1))
     verts[..., 0] = r * np.cos(th)  # x = r cos(th)
     verts[..., 1] = r * np.sin(th)  # y = r sin(th)
     # just reshape: no redundant vertices...
     verts = verts.reshape((rows+1)*cols, 3)
-    ## compute faces
-    faces = np.empty((rows*cols*2, 3), dtype=np.uint)
+    # compute faces
+    faces = np.empty((rows*cols*2, 3), dtype=np.uint32)
     rowtemplate1 = (((np.arange(cols).reshape(cols, 1) +
-                      np.array([[0, 1, 0]])) % cols)
-                    + np.array([[0, 0, cols]]))
+                      np.array([[0, 1, 0]])) % cols) +
+                    np.array([[0, 0, cols]]))
     rowtemplate2 = (((np.arange(cols).reshape(cols, 1) +
-                      np.array([[0, 1, 1]])) % cols)
-                    + np.array([[cols, 0, cols]]))
+                      np.array([[0, 1, 1]])) % cols) +
+                    np.array([[cols, 0, cols]]))
     for row in range(rows):
         start = row * cols * 2
         faces[start:start+cols] = rowtemplate1 + row * cols
         faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols
 
     return MeshData(vertices=verts, faces=faces)
+
+
+def create_cone(cols, radius=1.0, length=1.0):
+    """Create a cone
+
+    Parameters
+    ----------
+    cols : int
+        Number of faces.
+    radius : float
+        Base cone radius.
+    length : float
+        Length of the cone.
+
+    Returns
+    -------
+    cone : MeshData
+        Vertices and faces computed for a cone surface.
+    """
+    verts = np.empty((cols+1, 3), dtype=np.float32)
+    # compute vertexes
+    th = np.linspace(2 * np.pi, 0, cols+1).reshape(1, cols+1)
+    verts[:-1, 2] = 0.0
+    verts[:-1, 0] = radius * np.cos(th[0, :-1])  # x = r cos(th)
+    verts[:-1, 1] = radius * np.sin(th[0, :-1])  # y = r sin(th)
+    # Add the extremity
+    verts[-1, 0] = 0.0
+    verts[-1, 1] = 0.0
+    verts[-1, 2] = length
+    verts = verts.reshape((cols+1), 3)  # just reshape: no redundant vertices
+    # compute faces
+    faces = np.empty((cols, 3), dtype=np.uint32)
+    template = np.array([[0, 1]])
+    for pos in range(cols):
+        faces[pos, :-1] = template + pos
+    faces[:, 2] = cols
+    faces[-1, 1] = 0
+
+    return MeshData(vertices=verts, faces=faces)
+
+
+def create_arrow(rows, cols, radius=0.1, length=1.0,
+                 cone_radius=None, cone_length=None):
+    """Create a 3D arrow using a cylinder plus cone
+
+    Parameters
+    ----------
+    rows : int
+        Number of rows.
+    cols : int
+        Number of columns.
+    radius : float
+        Base cylinder radius.
+    length : float
+        Length of the arrow.
+    cone_radius : float
+        Radius of the cone base.
+           If None, then this defaults to 2x the cylinder radius.
+    cone_length : float
+        Length of the cone.
+           If None, then this defaults to 1/3 of the arrow length.
+
+    Returns
+    -------
+    arrow : MeshData
+        Vertices and faces computed for a cone surface.
+    """
+    # create the cylinder
+    md_cyl = None
+    if cone_radius is None:
+        cone_radius = radius*2.0
+    if cone_length is None:
+        con_L = length/3.0
+        cyl_L = length*2.0/3.0
+    else:
+        cyl_L = max(0, length - cone_length)
+        con_L = min(cone_length, length)
+    if cyl_L != 0:
+        md_cyl = create_cylinder(rows, cols, radius=[radius, radius],
+                                 length=cyl_L)
+    # create the cone
+    md_con = create_cone(cols, radius=cone_radius, length=con_L)
+    verts = md_con.get_vertices()
+    nbr_verts_con = verts.size//3
+    faces = md_con.get_faces()
+    if md_cyl is not None:
+        trans = np.array([[0.0, 0.0, cyl_L]])
+        verts = np.vstack((verts+trans, md_cyl.get_vertices()))
+        faces = np.vstack((faces, md_cyl.get_faces()+nbr_verts_con))
+
+    return MeshData(vertices=verts, faces=faces)
diff --git a/vispy/geometry/isocurve.py b/vispy/geometry/isocurve.py
index 09a816f..ab89928 100644
--- a/vispy/geometry/isocurve.py
+++ b/vispy/geometry/isocurve.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
@@ -83,7 +83,7 @@ def isocurve(data, level, connected=False, extend_to_edge=False):
         for j in [0, 1]:
             fields[i, j] = mask[slices[i], slices[j]]
             vertIndex = i+2*j
-            index += fields[i, j] * 2**vertIndex
+            index += (fields[i, j] * 2**vertIndex).astype(np.ubyte)
     
     # add lines
     for i in range(index.shape[0]):                 # data x-axis
diff --git a/vispy/geometry/isosurface.py b/vispy/geometry/isosurface.py
index 550db48..07d275f 100644
--- a/vispy/geometry/isosurface.py
+++ b/vispy/geometry/isosurface.py
@@ -38,7 +38,7 @@ def isosurface(data, level):
                 fields[i, j, k] = mask[slices[i], slices[j], slices[k]]
                 # this is just to match Bourk's vertex numbering scheme:
                 vertIndex = i - 2*j*i + 3*j + 4*k
-                index += fields[i, j, k] * 2**vertIndex
+                index += (fields[i, j, k] * 2**vertIndex).astype(np.ubyte)
     
     ### Generate table of edges that have been cut
     cut_edges = np.zeros([x+1 for x in index.shape]+[3], dtype=np.uint32)
@@ -52,7 +52,7 @@ def isosurface(data, level):
     # generate vertex positions
     m = cut_edges > 0
     vertex_inds = np.argwhere(m)  # argwhere is slow!
-    vertexes = vertex_inds[:, :3].astype(np.float32)
+    vertexes = vertex_inds[:, :3].astype(np.float32).copy()
     dataFlat = data.reshape(data.shape[0]*data.shape[1]*data.shape[2])
     
     ## re-use the cut_edges array as a lookup table for vertex IDs
@@ -115,7 +115,8 @@ def isosurface(data, level):
         # expensive:
         verts = face_shift_tables[i][cellInds]
         # we now have indexes into cut_edges:
-        verts[..., :3] += cells[:, np.newaxis, np.newaxis, :]
+        verts[..., :3] += (cells[:, np.newaxis,
+                                 np.newaxis, :]).astype(np.uint16)
         verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:])
         
         # expensive:
diff --git a/vispy/geometry/meshdata.py b/vispy/geometry/meshdata.py
index 32a8e7f..559b541 100644
--- a/vispy/geometry/meshdata.py
+++ b/vispy/geometry/meshdata.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
@@ -7,6 +7,20 @@ import numpy as np
 from ..ext.six.moves import xrange
 
 
+def _fix_colors(colors):
+    colors = np.asarray(colors)
+    if colors.ndim not in (2, 3):
+        raise ValueError('colors must have 2 or 3 dimensions')
+    if colors.shape[-1] not in (3, 4):
+        raise ValueError('colors must have 3 or 4 elements')
+    if colors.shape[-1] == 3:
+        pad = np.ones((len(colors), 1), colors.dtype)
+        if colors.ndim == 3:
+            pad = pad[:, :, np.newaxis]
+        colors = np.concatenate((colors, pad), axis=-1)
+    return colors
+
+
 class MeshData(object):
     """
     Class for storing and operating on 3D mesh data.
@@ -54,32 +68,34 @@ class MeshData(object):
         self._vertices_indexed_by_faces = None  # (Nf, 3, 3) vertex coordinates
         self._vertices_indexed_by_edges = None  # (Ne, 2, 3) vertex coordinates
 
-        ## mappings between vertices, faces, and edges
+        # mappings between vertices, faces, and edges
         self._faces = None  # Nx3 indices into self._vertices, 3 verts/face
         self._edges = None  # Nx2 indices into self._vertices, 2 verts/edge
+        self._edges_indexed_by_faces = None  # (Ne, 3, 2) indices into
+        # self._vertices, 3 edge / face and 2 verts/edge
         # inverse mappings
         self._vertex_faces = None  # maps vertex ID to a list of face IDs
         self._vertex_edges = None  # maps vertex ID to a list of edge IDs
 
-        ## Per-vertex data
+        # Per-vertex data
         self._vertex_normals = None                # (Nv, 3) normals
         self._vertex_normals_indexed_by_faces = None  # (Nf, 3, 3) normals
         self._vertex_colors = None                 # (Nv, 3) colors
         self._vertex_colors_indexed_by_faces = None   # (Nf, 3, 4) colors
         self._vertex_colors_indexed_by_edges = None   # (Nf, 2, 4) colors
 
-        ## Per-face data
+        # Per-face data
         self._face_normals = None                # (Nf, 3) face normals
         self._face_normals_indexed_by_faces = None  # (Nf, 3, 3) face normals
         self._face_colors = None                 # (Nf, 4) face colors
         self._face_colors_indexed_by_faces = None   # (Nf, 3, 4) face colors
         self._face_colors_indexed_by_edges = None   # (Ne, 2, 4) face colors
 
-        ## Per-edge data
+        # Per-edge data
         self._edge_colors = None                # (Ne, 4) edge colors
         self._edge_colors_indexed_by_edges = None  # (Ne, 2, 4) edge colors
         # default color to use if no face/edge/vertex colors are given
-        #self._meshColor = (1, 1, 1, 0.1)
+        # self._meshColor = (1, 1, 1, 0.1)
 
         if vertices is not None:
             if faces is None:
@@ -96,37 +112,77 @@ class MeshData(object):
                 if face_colors is not None:
                     self.set_face_colors(face_colors)
 
-    def faces(self):
+    def get_faces(self):
         """Array (Nf, 3) of vertex indices, three per triangular face.
 
         If faces have not been computed for this mesh, returns None.
         """
         return self._faces
 
-    def edges(self):
-        """Array (Nf, 3) of vertex indices, two per edge in the mesh."""
-        if self._edges is None:
-            self._compute_edges()
-        return self._edges
+    def get_edges(self, indexed=None):
+        """Edges of the mesh
+        
+        Parameters
+        ----------
+        indexed : str | None
+           If indexed is None, return (Nf, 3) array of vertex indices,
+           two per edge in the mesh.
+           If indexed is 'faces', then return (Nf, 3, 2) array of vertex
+           indices with 3 edges per face, and two vertices per edge.
+
+        Returns
+        -------
+        edges : ndarray
+            The edges.
+        """
+        
+        if indexed is None:
+            if self._edges is None:
+                self._compute_edges(indexed=None)
+            return self._edges
+        elif indexed == 'faces':
+            if self._edges_indexed_by_faces is None:
+                self._compute_edges(indexed='faces')
+            return self._edges_indexed_by_faces
+        else:
+            raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
 
     def set_faces(self, faces):
-        """Set the (Nf, 3) array of faces. Each rown in the array contains
-        three indices into the vertex array, specifying the three corners
-        of a triangular face."""
+        """Set the faces
+
+        Parameters
+        ----------
+        faces : ndarray
+            (Nf, 3) array of faces. Each row in the array contains
+            three indices into the vertex array, specifying the three corners
+            of a triangular face.
+        """
         self._faces = faces
         self._edges = None
+        self._edges_indexed_by_faces = None
         self._vertex_faces = None
         self._vertices_indexed_by_faces = None
         self.reset_normals()
         self._vertex_colors_indexed_by_faces = None
         self._face_colors_indexed_by_faces = None
 
-    def vertices(self, indexed=None):
-        """Return an array (N,3) of the positions of vertices in the mesh.
-        By default, each unique vertex appears only once in the array.
-        If indexed is 'faces', then the array will instead contain three
-        vertices per face in the mesh (and a single vertex may appear more
-        than once in the array)."""
+    def get_vertices(self, indexed=None):
+        """Get the vertices
+
+        Parameters
+        ----------
+        indexed : str | None
+            If Note, return an array (N,3) of the positions of vertices in
+            the mesh. By default, each unique vertex appears only once.
+            If indexed is 'faces', then the array will instead contain three
+            vertices per face in the mesh (and a single vertex may appear more
+            than once in the array).
+
+        Returns
+        -------
+        vertices : ndarray
+            The vertices.
+        """
         if indexed is None:
             if (self._vertices is None and
                     self._vertices_indexed_by_faces is not None):
@@ -135,18 +191,43 @@ class MeshData(object):
         elif indexed == 'faces':
             if (self._vertices_indexed_by_faces is None and
                     self._vertices is not None):
-                self._vertices_indexed_by_faces = self._vertices[self.faces()]
+                self._vertices_indexed_by_faces = \
+                    self._vertices[self.get_faces()]
             return self._vertices_indexed_by_faces
         else:
             raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
 
-    def set_vertices(self, verts=None, indexed=None, reset_normals=True):
+    def get_bounds(self):
+        """Get the mesh bounds
+
+        Returns
+        -------
+        bounds : list
+            A list of tuples of mesh bounds.
         """
-        Set the array (Nv, 3) of vertex coordinates.
-        If indexed=='faces', then the data must have shape (Nf, 3, 3) and is
-        assumed to be already indexed as a list of faces.
-        This will cause any pre-existing normal vectors to be cleared
-        unless reset_normals=False.
+        if self._vertices_indexed_by_faces is not None:
+            v = self._vertices_indexed_by_faces
+        elif self._vertices is not None:
+            v = self._vertices
+        else:
+            return None
+        bounds = [(v[:, ax].min(), v[:, ax].max()) for ax in range(v.shape[1])]
+        return bounds
+        
+    def set_vertices(self, verts=None, indexed=None, reset_normals=True):
+        """Set the mesh vertices
+
+        Parameters
+        ----------
+        verts : ndarray | None
+            The array (Nv, 3) of vertex coordinates.
+        indexed : str | None
+            If indexed=='faces', then the data must have shape (Nf, 3, 3) and
+            is assumed to be already indexed as a list of faces. This will
+            cause any pre-existing normal vectors to be cleared unless
+            reset_normals=False.
+        reset_normals : bool
+            If True, reset the normals.
         """
         if indexed is None:
             if verts is not None:
@@ -192,15 +273,23 @@ class MeshData(object):
                 return True
         return False
 
-    def face_normals(self, indexed=None):
-        """
-        Return an array (Nf, 3) of normal vectors for each face.
-        If indexed='faces', then instead return an indexed array
-        (Nf, 3, 3)  (this is just the same array with each vector
-        copied three times).
+    def get_face_normals(self, indexed=None):
+        """Get face normals
+
+        Parameters
+        ----------
+        indexed : str | None
+            If None, return an array (Nf, 3) of normal vectors for each face.
+            If 'faces', then instead return an indexed array (Nf, 3, 3)
+            (this is just the same array with each vector copied three times).
+
+        Returns
+        -------
+        normals : ndarray
+            The normals.
         """
         if self._face_normals is None:
-            v = self.vertices(indexed='faces')
+            v = self.get_vertices(indexed='faces')
             self._face_normals = np.cross(v[:, 1] - v[:, 0],
                                           v[:, 2] - v[:, 0])
 
@@ -216,17 +305,25 @@ class MeshData(object):
         else:
             raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
 
-    def vertex_normals(self, indexed=None):
-        """
-        Return an array of normal vectors.
-        By default, the array will be (N, 3) with one entry per unique
-        vertex in the mesh. If indexed is 'faces', then the array will
-        contain three normal vectors per face (and some vertices may be
-        repeated).
+    def get_vertex_normals(self, indexed=None):
+        """Get vertex normals
+
+        Parameters
+        ----------
+        indexed : str | None
+            If None, return an (N, 3) array of normal vectors with one entry
+            per unique vertex in the mesh. If indexed is 'faces', then the
+            array will contain three normal vectors per face (and some
+            vertices may be repeated).
+
+        Returns
+        -------
+        normals : ndarray
+            The normals.
         """
         if self._vertex_normals is None:
-            faceNorms = self.face_normals()
-            vertFaces = self.vertex_faces()
+            faceNorms = self.get_face_normals()
+            vertFaces = self.get_vertex_faces()
             self._vertex_normals = np.empty(self._vertices.shape,
                                             dtype=np.float32)
             for vindex in xrange(self._vertices.shape[0]):
@@ -242,47 +339,80 @@ class MeshData(object):
         if indexed is None:
             return self._vertex_normals
         elif indexed == 'faces':
-            return self._vertex_normals[self.faces()]
+            return self._vertex_normals[self.get_faces()]
         else:
             raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
 
-    def vertex_colors(self, indexed=None):
-        """
-        Return an array (Nv, 4) of vertex colors.
-        If indexed=='faces', then instead return an indexed array
-        (Nf, 3, 4).
+    def get_vertex_colors(self, indexed=None):
+        """Get vertex colors
+
+        Parameters
+        ----------
+        indexed : str | None
+            If None, return an array (Nv, 4) of vertex colors.
+            If indexed=='faces', then instead return an indexed array
+            (Nf, 3, 4).
+
+        Returns
+        -------
+        colors : ndarray
+            The vertex colors.
         """
         if indexed is None:
             return self._vertex_colors
         elif indexed == 'faces':
             if self._vertex_colors_indexed_by_faces is None:
                 self._vertex_colors_indexed_by_faces = \
-                    self._vertex_colors[self.faces()]
+                    self._vertex_colors[self.get_faces()]
             return self._vertex_colors_indexed_by_faces
         else:
             raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
 
     def set_vertex_colors(self, colors, indexed=None):
+        """Set the vertex color array
+
+        Parameters
+        ----------
+        colors : array
+            Array of colors. Must have shape (Nv, 4) (indexing by vertex)
+            or shape (Nf, 3, 4) (vertices indexed by face).
+        indexed : str | None
+            Should be 'faces' if colors are indexed by faces.
         """
-        Set the vertex color array (Nv, 4).
-        If indexed=='faces', then the array will be interpreted
-        as indexed and should have shape (Nf, 3, 4)
-        """
+        colors = _fix_colors(np.asarray(colors))
         if indexed is None:
+            if colors.ndim != 2:
+                raise ValueError('colors must be 2D if indexed is None')
+            if colors.shape[0] != self.n_vertices:
+                raise ValueError('incorrect number of colors %s, expected %s'
+                                 % (colors.shape[0], self.n_vertices))
             self._vertex_colors = colors
             self._vertex_colors_indexed_by_faces = None
         elif indexed == 'faces':
+            if colors.ndim != 3:
+                raise ValueError('colors must be 3D if indexed is "faces"')
+            if colors.shape[0] != self.n_faces:
+                raise ValueError('incorrect number of faces')
             self._vertex_colors = None
             self._vertex_colors_indexed_by_faces = colors
         else:
-            raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
-
-    def face_colors(self, indexed=None):
-        """
-        Return an array (Nf, 4) of face colors.
-        If indexed=='faces', then instead return an indexed array
-        (Nf, 3, 4)  (note this is just the same array with each color
-        repeated three times).
+            raise ValueError('indexed must be None or "faces"')
+
+    def get_face_colors(self, indexed=None):
+        """Get the face colors
+
+        Parameters
+        ----------
+        indexed : str | None
+            If indexed is None, return (Nf, 4) array of face colors.
+            If indexed=='faces', then instead return an indexed array
+            (Nf, 3, 4)  (note this is just the same array with each color
+            repeated three times).
+        
+        Returns
+        -------
+        colors : ndarray
+            The colors.
         """
         if indexed is None:
             return self._face_colors
@@ -299,47 +429,61 @@ class MeshData(object):
             raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
 
     def set_face_colors(self, colors, indexed=None):
+        """Set the face color array
+
+        Parameters
+        ----------
+        colors : array
+            Array of colors. Must have shape (Nf, 4) (indexed by face),
+            or shape (Nf, 3, 4) (face colors indexed by faces).
+        indexed : str | None
+            Should be 'faces' if colors are indexed by faces.
         """
-        Set the face color array (Nf, 4).
-        If indexed=='faces', then the array will be interpreted
-        as indexed and should have shape (Nf, 3, 4)
-        """
+        colors = _fix_colors(colors)
+        if colors.shape[0] != self.n_faces:
+            raise ValueError('incorrect number of colors %s, expected %s'
+                             % (colors.shape[0], self.n_faces))
         if indexed is None:
+            if colors.ndim != 2:
+                raise ValueError('colors must be 2D if indexed is None')
             self._face_colors = colors
             self._face_colors_indexed_by_faces = None
         elif indexed == 'faces':
+            if colors.ndim != 3:
+                raise ValueError('colors must be 3D if indexed is "faces"')
             self._face_colors = None
             self._face_colors_indexed_by_faces = colors
         else:
-            raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
+            raise ValueError('indexed must be None or "faces"')
 
-    def face_count(self):
-        """
-        Return the number of faces in the mesh.
-        """
+    @property
+    def n_faces(self):
+        """The number of faces in the mesh"""
         if self._faces is not None:
             return self._faces.shape[0]
         elif self._vertices_indexed_by_faces is not None:
             return self._vertices_indexed_by_faces.shape[0]
 
-    def edge_colors(self):
-        return self._edge_colors
+    @property
+    def n_vertices(self):
+        """The number of vertices in the mesh"""
+        if self._vertices is None:
+            self._compute_unindexed_vertices()
+        return len(self._vertices)
 
-    #def _set_indexed_faces(self, faces, vertex_colors=None, face_colors=None):
-        #self._vertices_indexed_by_faces = faces
-        #self._vertex_colors_indexed_by_faces = vertex_colors
-        #self._face_colors_indexed_by_faces = face_colors
+    def get_edge_colors(self):
+        return self._edge_colors
 
     def _compute_unindexed_vertices(self):
-        ## Given (Nv, 3, 3) array of vertices-indexed-by-face, convert
-        ## backward to unindexed vertices
-        ## This is done by collapsing into a list of 'unique' vertices
-        ## (difference < 1e-14)
+        # Given (Nv, 3, 3) array of vertices-indexed-by-face, convert
+        # backward to unindexed vertices
+        # This is done by collapsing into a list of 'unique' vertices
+        # (difference < 1e-14)
 
-        ## I think generally this should be discouraged..
+        # I think generally this should be discouraged..
         faces = self._vertices_indexed_by_faces
         verts = {}  # used to remember the index of each vertex position
-        self._faces = np.empty(faces.shape[:2], dtype=np.uint)
+        self._faces = np.empty(faces.shape[:2], dtype=np.uint32)
         self._vertices = []
         self._vertex_faces = []
         self._face_normals = None
@@ -352,7 +496,6 @@ class MeshData(object):
                 pt2 = tuple([round(x*1e14) for x in pt])
                 index = verts.get(pt2, None)
                 if index is None:
-                    #self._vertices.append(QtGui.QVector3D(*pt))
                     self._vertices.append(pt)
                     self._vertex_faces.append([])
                     index = len(self._vertices)-1
@@ -362,76 +505,62 @@ class MeshData(object):
                 self._faces[i, j] = index
         self._vertices = np.array(self._vertices, dtype=np.float32)
 
-    #def _setUnindexedFaces(self, faces, vertices, vertex_colors=None,
-        #                   face_colors=None):
-        #self._vertices = vertices #[QtGui.QVector3D(*v) for v in vertices]
-        #self._faces = faces.astype(np.uint)
-        #self._edges = None
-        #self._vertex_faces = None
-        #self._face_normals = None
-        #self._vertex_normals = None
-        #self._vertex_colors = vertex_colors
-        #self._face_colors = face_colors
-
-    def vertex_faces(self):
+    def get_vertex_faces(self):
         """
         List mapping each vertex index to a list of face indices that use it.
         """
         if self._vertex_faces is None:
-            self._vertex_faces = [[] for i in xrange(len(self.vertices()))]
+            self._vertex_faces = [[] for i in xrange(len(self.get_vertices()))]
             for i in xrange(self._faces.shape[0]):
                 face = self._faces[i]
                 for ind in face:
                     self._vertex_faces[ind].append(i)
         return self._vertex_faces
 
-    #def reverseNormals(self):
-        #"""
-        #Reverses the direction of all normal vectors.
-        #"""
-        #pass
-
-    #def generateEdgesFromFaces(self):
-        #"""
-        #Generate a set of edges by listing all the edges of faces and
-        #removing any duplicates.
-        #Useful for displaying wireframe meshes.
-        #"""
-        #pass
-
-    def _compute_edges(self):
-        if not self.has_face_indexed_data:
-            ## generate self._edges from self._faces
-            nf = len(self._faces)
-            edges = np.empty(nf*3, dtype=[('i', np.uint, 2)])
-            edges['i'][0:nf] = self._faces[:, :2]
-            edges['i'][nf:2*nf] = self._faces[:, 1:3]
-            edges['i'][-nf:, 0] = self._faces[:, 2]
-            edges['i'][-nf:, 1] = self._faces[:, 0]
-
-            # sort per-edge
-            mask = edges['i'][:, 0] > edges['i'][:, 1]
-            edges['i'][mask] = edges['i'][mask][:, ::-1]
-
-            # remove duplicate entries
-            self._edges = np.unique(edges)['i']
-        elif self._vertices_indexed_by_faces is not None:
-            verts = self._vertices_indexed_by_faces
-            edges = np.empty((verts.shape[0], 3, 2), dtype=np.uint)
-            nf = verts.shape[0]
-            edges[:, 0, 0] = np.arange(nf) * 3
-            edges[:, 0, 1] = edges[:, 0, 0] + 1
-            edges[:, 1, 0] = edges[:, 0, 1]
-            edges[:, 1, 1] = edges[:, 1, 0] + 1
-            edges[:, 2, 0] = edges[:, 1, 1]
-            edges[:, 2, 1] = edges[:, 0, 0]
-            self._edges = edges
+    def _compute_edges(self, indexed=None):
+        if indexed is None:
+            if self._faces is not None:
+                # generate self._edges from self._faces
+                nf = len(self._faces)
+                edges = np.empty(nf*3, dtype=[('i', np.uint32, 2)])
+                edges['i'][0:nf] = self._faces[:, :2]
+                edges['i'][nf:2*nf] = self._faces[:, 1:3]
+                edges['i'][-nf:, 0] = self._faces[:, 2]
+                edges['i'][-nf:, 1] = self._faces[:, 0]
+                # sort per-edge
+                mask = edges['i'][:, 0] > edges['i'][:, 1]
+                edges['i'][mask] = edges['i'][mask][:, ::-1]
+                # remove duplicate entries
+                self._edges = np.unique(edges)['i']
+            else:
+                raise Exception("MeshData cannot generate edges--no faces in "
+                                "this data.")
+        elif indexed == 'faces':
+            if self._vertices_indexed_by_faces is not None:
+                verts = self._vertices_indexed_by_faces
+                edges = np.empty((verts.shape[0], 3, 2), dtype=np.uint32)
+                nf = verts.shape[0]
+                edges[:, 0, 0] = np.arange(nf) * 3
+                edges[:, 0, 1] = edges[:, 0, 0] + 1
+                edges[:, 1, 0] = edges[:, 0, 1]
+                edges[:, 1, 1] = edges[:, 1, 0] + 1
+                edges[:, 2, 0] = edges[:, 1, 1]
+                edges[:, 2, 1] = edges[:, 0, 0]
+                self._edges_indexed_by_faces = edges
+            else:
+                raise Exception("MeshData cannot generate edges--no faces in "
+                                "this data.")
         else:
-            raise Exception("MeshData cannot generate edges--no faces in "
-                            "this data.")
+            raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
 
     def save(self):
-        """Serialize this mesh to a string appropriate for disk storage"""
+        """Serialize this mesh to a string appropriate for disk storage
+
+        Returns
+        -------
+        state : dict
+            The state.
+        """
         import pickle
         if self._faces is not None:
             names = ['_vertices', '_faces']
@@ -452,7 +581,13 @@ class MeshData(object):
         return pickle.dumps(state)
 
     def restore(self, state):
-        """Restore the state of a mesh previously saved using save()"""
+        """Restore the state of a mesh previously saved using save()
+
+        Parameters
+        ----------
+        state : dict
+            The previous state.
+        """
         import pickle
         state = pickle.loads(state)
         for k in state:
diff --git a/vispy/geometry/normals.py b/vispy/geometry/normals.py
new file mode 100644
index 0000000..83a754e
--- /dev/null
+++ b/vispy/geometry/normals.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+import numpy as np
+
+
+def compact(vertices, indices, tolerance=1e-3):
+    """ Compact vertices and indices within given tolerance """
+
+    # Transform vertices into a structured array for np.unique to work
+    n = len(vertices)
+    V = np.zeros(n, dtype=[("pos", np.float32, 3)])
+    V["pos"][:, 0] = vertices[:, 0]
+    V["pos"][:, 1] = vertices[:, 1]
+    V["pos"][:, 2] = vertices[:, 2]
+
+    epsilon = 1e-3
+    decimals = int(np.log(epsilon)/np.log(1/10.))
+
+    # Round all vertices within given decimals
+    V_ = np.zeros_like(V)
+    X = V["pos"][:, 0].round(decimals=decimals)
+    X[np.where(abs(X) < epsilon)] = 0
+
+    V_["pos"][:, 0] = X
+    Y = V["pos"][:, 1].round(decimals=decimals)
+    Y[np.where(abs(Y) < epsilon)] = 0
+    V_["pos"][:, 1] = Y
+
+    Z = V["pos"][:, 2].round(decimals=decimals)
+    Z[np.where(abs(Z) < epsilon)] = 0
+    V_["pos"][:, 2] = Z
+
+    # Find the unique vertices AND the mapping
+    U, RI = np.unique(V_, return_inverse=True)
+
+    # Translate indices from original vertices into the reduced set (U)
+    indices = indices.ravel()
+    I_ = indices.copy().ravel()
+    for i in range(len(indices)):
+        I_[i] = RI[indices[i]]
+    I_ = I_.reshape(len(indices)/3, 3)
+
+    # Return reduced vertices set, transalted indices and mapping that allows
+    # to go from U to V
+    return U.view(np.float32).reshape(len(U), 3), I_, RI
+
+
+def normals(vertices, indices):
+    """
+    Compute normals over a triangulated surface
+
+    Parameters
+    ----------
+
+    vertices : ndarray (n,3)
+        triangles vertices
+
+    indices : ndarray (p,3)
+        triangles indices
+    """
+
+    # Compact similar vertices
+    vertices, indices, mapping = compact(vertices, indices)
+
+    T = vertices[indices]
+    N = np.cross(T[:, 1] - T[:, 0], T[:, 2]-T[:, 0])
+    L = np.sqrt(np.sum(N * N, axis=1))
+    L[L == 0] = 1.0  # prevent divide-by-zero
+    N /= L[:, np.newaxis]
+    normals = np.zeros_like(vertices)
+    normals[indices[:, 0]] += N
+    normals[indices[:, 1]] += N
+    normals[indices[:, 2]] += N
+    L = np.sqrt(np.sum(normals*normals, axis=1))
+    L[L == 0] = 1.0
+    normals /= L[:, np.newaxis]
+
+    return normals[mapping]
diff --git a/vispy/geometry/parametric.py b/vispy/geometry/parametric.py
new file mode 100644
index 0000000..eaaf0fd
--- /dev/null
+++ b/vispy/geometry/parametric.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+import numpy as np
+from .normals import normals
+
+
+def surface(func, umin=0, umax=2 * np.pi, ucount=64, urepeat=1.0,
+            vmin=0, vmax=2 * np.pi, vcount=64, vrepeat=1.0):
+    """
+    Computes the parameterization of a parametric surface
+
+    func: function(u,v)
+        Parametric function used to build the surface
+    """
+
+    vtype = [('position', np.float32, 3),
+             ('texcoord', np.float32, 2),
+             ('normal',   np.float32, 3)]
+    itype = np.uint32
+
+    # umin, umax, ucount = 0, 2*np.pi, 64
+    # vmin, vmax, vcount = 0, 2*np.pi, 64
+
+    vcount += 1
+    ucount += 1
+    n = vcount * ucount
+
+    Un = np.repeat(np.linspace(0, 1, ucount, endpoint=True), vcount)
+    Vn = np.tile(np.linspace(0, 1, vcount, endpoint=True), ucount)
+    U = umin + Un * (umax - umin)
+    V = vmin + Vn * (vmax - vmin)
+
+    vertices = np.zeros(n, dtype=vtype)
+    for i, (u, v) in enumerate(zip(U, V)):
+        vertices["position"][i] = func(u, v)
+
+    vertices["texcoord"][:, 0] = Un * urepeat
+    vertices["texcoord"][:, 1] = Vn * vrepeat
+
+    indices = []
+    for i in range(ucount - 1):
+        for j in range(vcount - 1):
+            indices.append(i * (vcount) + j)
+            indices.append(i * (vcount) + j + 1)
+            indices.append(i * (vcount) + j + vcount + 1)
+            indices.append(i * (vcount) + j + vcount)
+            indices.append(i * (vcount) + j + vcount + 1)
+            indices.append(i * (vcount) + j)
+    indices = np.array(indices, dtype=itype)
+    vertices["normal"] = normals(vertices["position"],
+                                 indices.reshape(len(indices) / 3, 3))
+
+    return vertices, indices
diff --git a/vispy/geometry/polygon.py b/vispy/geometry/polygon.py
index c43ff4a..b3f8254 100644
--- a/vispy/geometry/polygon.py
+++ b/vispy/geometry/polygon.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
@@ -17,10 +17,10 @@ class PolygonData(object):
     vertices : (Nv, 3) array
         Vertex coordinates. If faces is not specified, then this will instead
         be interpreted as (Nf, 3, 3) array of coordinates.
-    faces : (Nf, 3) array
-        Indexes into the vertex array.
     edges : (Nv, 2) array
         Constraining edges specified by vertex indices.
+    faces : (Nf, 3) array
+        Indexes into the vertex array.
 
     Notes
     -----
@@ -116,13 +116,13 @@ class PolygonData(object):
         npts = self._vertices.shape[0]
         if np.any(self._vertices[0] != self._vertices[1]):
             # start != end, so edges must wrap around to beginning.
-            edges = np.empty((npts, 2), dtype=np.uint)
+            edges = np.empty((npts, 2), dtype=np.uint32)
             edges[:, 0] = np.arange(npts)
             edges[:, 1] = edges[:, 0] + 1
             edges[-1, 1] = 0
         else:
             # start == end; no wrapping required.
-            edges = np.empty((npts-1, 2), dtype=np.uint)
+            edges = np.empty((npts-1, 2), dtype=np.uint32)
             edges[:, 0] = np.arange(npts)
             edges[:, 1] = edges[:, 0] + 1
 
@@ -133,5 +133,10 @@ class PolygonData(object):
     def add_vertex(self, vertex):
         """
         Adds given vertex and retriangulates to generate new faces.
+
+        Parameters
+        ----------
+        vertex : array-like
+            The vertex to add.
         """
-        pass
+        raise NotImplementedError
diff --git a/vispy/geometry/rect.py b/vispy/geometry/rect.py
index e41aa44..c7be50e 100644
--- a/vispy/geometry/rect.py
+++ b/vispy/geometry/rect.py
@@ -11,13 +11,17 @@ class Rect(object):
         Can be in the form `Rect(x, y, w, h)`, `Rect(pos, size)`, or
         `Rect(Rect)`.
     """
-    def __init__(self, *args, **kwds):
+    def __init__(self, *args, **kwargs):
         self._pos = (0, 0)
         self._size = (0, 0)
 
         if len(args) == 1 and isinstance(args[0], Rect):
             self._pos = args[0]._pos
             self._size = args[0]._size
+        elif (len(args) == 1 and isinstance(args[0], (list, tuple)) and
+              len(args[0]) == 4):
+            self._pos = args[0][:2]
+            self._size = args[0][2:]
         elif len(args) == 2:
             self._pos = tuple(args[0])
             self._size = tuple(args[1])
@@ -28,8 +32,8 @@ class Rect(object):
             raise TypeError("Rect must be instantiated with 0, 1, 2, or 4 "
                             "non-keyword arguments.")
 
-        self._pos = kwds.get('pos', self._pos)
-        self._size = kwds.get('size', self._size)
+        self._pos = kwargs.get('pos', self._pos)
+        self._size = kwargs.get('size', self._size)
 
         if len(self._pos) != 2 or len(self._size) != 2:
             raise ValueError("Rect pos and size arguments must have 2 "
@@ -37,7 +41,7 @@ class Rect(object):
 
     @property
     def pos(self):
-        return self._pos
+        return tuple(self._pos)
 
     @pos.setter
     def pos(self, p):
@@ -46,7 +50,7 @@ class Rect(object):
 
     @property
     def size(self):
-        return self._size
+        return tuple(self._size)
 
     @size.setter
     def size(self, s):
@@ -104,7 +108,18 @@ class Rect(object):
         self.size = (self.size[0], y - self.pos[1])
 
     def padded(self, padding):
-        """Return a new Rect padded (smaller) by *padding* on all sides."""
+        """Return a new Rect padded (smaller) by padding on all sides
+
+        Parameters
+        ----------
+        padding : float
+            The padding.
+
+        Returns
+        -------
+        rect : instance of Rect
+            The padded rectangle.
+        """
         return Rect(pos=(self.pos[0]+padding, self.pos[1]+padding),
                     size=(self.size[0]-2*padding, self.size[1]-2*padding))
 
@@ -116,8 +131,19 @@ class Rect(object):
                     size=(abs(self.width), abs(self.height)))
 
     def flipped(self, x=False, y=True):
-        """ Return a Rect with the same bounds, but with the x or y axes 
-        inverted.
+        """Return a Rect with the same bounds but with axes inverted
+
+        Parameters
+        ----------
+        x : bool
+            Flip the X axis.
+        y : bool
+            Flip the Y axis.
+
+        Returns
+        -------
+        rect : instance of Rect
+            The flipped rectangle.
         """
         pos = list(self.pos)
         size = list(self.size)
@@ -139,6 +165,20 @@ class Rect(object):
         return self._transform_out(self._transform_in()[:, :2] + a[:2])
 
     def contains(self, x, y):
+        """Query if the rectangle contains points
+
+        Parameters
+        ----------
+        x : float
+            X coordinate.
+        y : float
+            Y coordinate.
+
+        Returns
+        -------
+        contains : bool
+            True if the point is within the rectangle.
+        """
         return (x >= self.left and x <= self.right and
                 y >= self.bottom and y <= self.top)
 
diff --git a/vispy/geometry/tests/test_calculations.py b/vispy/geometry/tests/test_calculations.py
new file mode 100644
index 0000000..90616e3
--- /dev/null
+++ b/vispy/geometry/tests/test_calculations.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+import numpy as np
+from numpy.testing import assert_allclose
+
+from vispy.testing import assert_raises
+from vispy.geometry import resize
+
+
+def test_resize():
+    """Test image resizing algorithms
+    """
+    assert_raises(ValueError, resize, np.zeros(3), (3, 3))
+    assert_raises(ValueError, resize, np.zeros((3, 3)), (3,))
+    assert_raises(ValueError, resize, np.zeros((3, 3)), (4, 4), kind='foo')
+    for kind, tol in (('nearest', 1e-5), ('linear', 2e-1)):
+        shape = np.array((10, 11, 3))
+        data = np.random.RandomState(0).rand(*shape)
+        assert_allclose(data, resize(data, shape[:2], kind=kind),
+                        rtol=1e-5, atol=1e-5)
+        # this won't actually be that close for bilinear interp
+        assert_allclose(data, resize(resize(data, 2 * shape[:2], kind=kind),
+                                     shape[:2], kind=kind), atol=tol, rtol=tol)
diff --git a/vispy/geometry/tests/test_generation.py b/vispy/geometry/tests/test_generation.py
index f28df9d..304d43b 100644
--- a/vispy/geometry/tests/test_generation.py
+++ b/vispy/geometry/tests/test_generation.py
@@ -1,9 +1,10 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import numpy as np
 from numpy.testing import assert_array_equal, assert_allclose
 
+from vispy.testing import run_tests_if_main
 from vispy.geometry import create_cube, create_cylinder, create_sphere
 
 
@@ -17,12 +18,15 @@ def test_cube():
 def test_sphere():
     """Test sphere function"""
     md = create_sphere(10, 20, radius=10)
-    radii = np.sqrt((md.vertices() ** 2).sum(axis=1))
+    radii = np.sqrt((md.get_vertices() ** 2).sum(axis=1))
     assert_allclose(radii, np.ones_like(radii) * 10)
 
 
 def test_cylinder():
     """Test cylinder function"""
     md = create_cylinder(10, 20, radius=[10, 10])
-    radii = np.sqrt((md.vertices()[:, :2] ** 2).sum(axis=1))
+    radii = np.sqrt((md.get_vertices()[:, :2] ** 2).sum(axis=1))
     assert_allclose(radii, np.ones_like(radii) * 10)
+
+
+run_tests_if_main()
diff --git a/vispy/geometry/tests/test_meshdata.py b/vispy/geometry/tests/test_meshdata.py
new file mode 100644
index 0000000..90e829f
--- /dev/null
+++ b/vispy/geometry/tests/test_meshdata.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+import numpy as np
+from numpy.testing import assert_array_equal
+
+from vispy.testing import run_tests_if_main
+from vispy.geometry.meshdata import MeshData
+
+
+def test_meshdata():
+    """Test meshdata Class
+       It's a unit square cut in two triangular element
+    """
+    square_vertices = np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]],
+                               dtype=np.float)
+    square_faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.uint)
+    square_normals = np.array([[0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]],
+                              dtype=np.float)
+    square_edges = np.array([[0, 1], [0, 2], [0, 3], [1, 2], [2, 3]],
+                            dtype=np.uint)
+
+    mesh = MeshData(vertices=square_vertices, faces=square_faces)
+    # test vertices and faces assignement
+    assert_array_equal(square_vertices, mesh.get_vertices())
+    assert_array_equal(square_faces, mesh.get_faces())
+    # test normals calculus
+    assert_array_equal(square_normals, mesh.get_vertex_normals())
+    # test edge calculus
+    assert_array_equal(square_edges, mesh.get_edges())
+
+
+run_tests_if_main()
diff --git a/vispy/geometry/tests/test_triangulation.py b/vispy/geometry/tests/test_triangulation.py
index 24cc50f..41b2df9 100644
--- a/vispy/geometry/tests/test_triangulation.py
+++ b/vispy/geometry/tests/test_triangulation.py
@@ -1,6 +1,7 @@
 import numpy as np
 from numpy.testing import assert_array_almost_equal
 
+from vispy.testing import run_tests_if_main
 from vispy.geometry.triangulation import Triangulation as T
 
 
@@ -35,15 +36,15 @@ def test_intersect_edge_arrays():
     t = T(pts, edges)
     
     # intersect array of one edge with a array of many edges
-    intercepts = t.intersect_edge_arrays(lines[0:1], lines[1:])
+    intercepts = t._intersect_edge_arrays(lines[0:1], lines[1:])
     expect = np.array([0.5, 0.0, 0.5, 1.0, np.nan])
     assert_array_eq(intercepts, expect)
 
     # intersect every line with every line
-    intercepts = t.intersect_edge_arrays(lines[:, np.newaxis, ...], 
-                                         lines[np.newaxis, ...])
+    intercepts = t._intersect_edge_arrays(lines[:, np.newaxis, ...], 
+                                          lines[np.newaxis, ...])
     for i in range(lines.shape[0]):
-        int2 = t.intersect_edge_arrays(lines[i], lines)
+        int2 = t._intersect_edge_arrays(lines[i], lines)
         assert_array_eq(intercepts[i], int2)
 
 
@@ -74,7 +75,7 @@ def test_edge_intersections():
     t = T(pts, edges)
     
     # first test find_edge_intersections
-    cuts = t.find_edge_intersections()
+    cuts = t._find_edge_intersections()
     expect = {
         0: [],
         1: [(0.5, [1., 0.5]), 
@@ -99,7 +100,7 @@ def test_edge_intersections():
                 assert_array_almost_equal(np.array(ecut[j]), np.array(vcut[j]))
                 
     # next test that we can split the edges correctly
-    t.split_intersecting_edges()
+    t._split_intersecting_edges()
     pts = np.array([[0., 0.],
                     [1., 0.],
                     [1., 1.],
@@ -147,7 +148,7 @@ def test_edge_intersections():
     
     edges = np.array([[0, 1], [1, 2], [2, 3], [3, 0]])
     t = T(pts, edges)
-    for edge, cuts in t.find_edge_intersections().items():
+    for edge, cuts in t._find_edge_intersections().items():
         assert len(cuts) == 0
 
     
@@ -171,7 +172,7 @@ def test_merge_duplicate_points():
     ])
     
     t = T(pts, edges)
-    t.merge_duplicate_points()
+    t._merge_duplicate_points()
 
     pts = np.array([
         [0, 0],
@@ -216,45 +217,45 @@ def test_utility_methods():
     # skip initialization and just simulate being part-way through 
     # triangulation
     for tri in [[0, 1, 4], [1, 2, 4], [2, 3, 4]]:
-        t.add_tri(*tri)
+        t._add_tri(*tri)
     
     # find_cut_triangle
-    assert t.find_cut_triangle((4, 5)) == (4, 1, 2)
+    assert t._find_cut_triangle((4, 5)) == (4, 1, 2)
     
     # orientation
-    assert t.orientation((4, 5), 0) == 1
-    assert t.orientation((4, 5), 1) == 1
-    assert t.orientation((4, 5), 2) == -1
-    assert t.orientation((4, 5), 3) == -1
-    assert t.orientation((4, 5), 4) == 0
-    assert t.orientation((4, 5), 5) == 0
+    assert t._orientation((4, 5), 0) == 1
+    assert t._orientation((4, 5), 1) == 1
+    assert t._orientation((4, 5), 2) == -1
+    assert t._orientation((4, 5), 3) == -1
+    assert t._orientation((4, 5), 4) == 0
+    assert t._orientation((4, 5), 5) == 0
     
     # distance
     dist = ((t.pts[0]-t.pts[1])**2).sum()**0.5
-    assert t.distance(t.pts[0], t.pts[1]) == dist
+    assert t._distance(t.pts[0], t.pts[1]) == dist
 
     # adjacent_tri
-    assert t.adjacent_tri((1, 4), 0) == (4, 1, 2)
-    assert t.adjacent_tri((0, 4), 1) is None
-    assert t.adjacent_tri((1, 4), (1, 4, 0)) == (4, 1, 2)
-    assert t.adjacent_tri((0, 4), (1, 4, 0)) is None
+    assert t._adjacent_tri((1, 4), 0) == (4, 1, 2)
+    assert t._adjacent_tri((0, 4), 1) is None
+    assert t._adjacent_tri((1, 4), (1, 4, 0)) == (4, 1, 2)
+    assert t._adjacent_tri((0, 4), (1, 4, 0)) is None
     try:
-        t.adjacent_tri((1, 4), 5)
+        t._adjacent_tri((1, 4), 5)
     except RuntimeError:
         pass
     else:
         raise Exception("Expected RuntimeError.")
 
     # edges_intersect
-    assert not t.edges_intersect((0, 1), (1, 2))
-    assert not t.edges_intersect((0, 2), (1, 2))
-    assert t.edges_intersect((4, 5), (1, 2))
+    assert not t._edges_intersect((0, 1), (1, 2))
+    assert not t._edges_intersect((0, 2), (1, 2))
+    assert t._edges_intersect((4, 5), (1, 2))
 
     # is_constraining_edge
-    assert t.is_constraining_edge((4, 5))
-    assert t.is_constraining_edge((5, 4))
-    assert not t.is_constraining_edge((3, 5))
-    assert not t.is_constraining_edge((3, 2))
+    assert t._is_constraining_edge((4, 5))
+    assert t._is_constraining_edge((5, 4))
+    assert not t._is_constraining_edge((3, 5))
+    assert not t._is_constraining_edge((3, 2))
 
 
 def test_projection():
@@ -265,12 +266,12 @@ def test_projection():
     t = T(pts, np.zeros((0, 2)))
     
     a, b, c, d = pts
-    assert np.allclose(t.projection(a, c, b), [1, 0]) 
-    assert np.allclose(t.projection(b, c, a), [1, 0]) 
-    assert np.allclose(t.projection(a, d, b), [3, 0]) 
-    assert np.allclose(t.projection(b, d, a), [3, 0]) 
-    assert np.allclose(t.projection(a, b, c), [1, 2]) 
-    assert np.allclose(t.projection(c, b, a), [1, 2]) 
+    assert np.allclose(t._projection(a, c, b), [1, 0]) 
+    assert np.allclose(t._projection(b, c, a), [1, 0]) 
+    assert np.allclose(t._projection(a, d, b), [3, 0]) 
+    assert np.allclose(t._projection(b, d, a), [3, 0]) 
+    assert np.allclose(t._projection(a, b, c), [1, 2]) 
+    assert np.allclose(t._projection(c, b, a), [1, 2]) 
 
     
 def test_random(): 
@@ -499,10 +500,6 @@ def test_edge_event():
 
     t = T(pts * [-1, 1], edges)
     t.triangulate()
-    
 
-if __name__ == '__main__':
-    #test_edge_intersections()
-    #test_merge_duplicate_points()
-    #test_utility_methods()
-    test_intersect_edge_arrays()
+
+run_tests_if_main()
diff --git a/vispy/geometry/torusknot.py b/vispy/geometry/torusknot.py
new file mode 100644
index 0000000..f2cc82f
--- /dev/null
+++ b/vispy/geometry/torusknot.py
@@ -0,0 +1,142 @@
+from __future__ import division
+
+import numpy as np
+from fractions import gcd
+
+
+class TorusKnot(object):
+    """Representation of a torus knot or link.
+
+    A torus knot is one that can be drawn on the surface of a
+    torus. It is parameterised by two integers p and q as below; in
+    fact this returns a single knot (a single curve) only if p and q
+    are coprime, otherwise it describes multiple linked curves.
+
+    Parameters
+    ----------
+    p : int
+        The number of times the knot winds around the outside of the
+        torus. Defaults to 2.
+    q : int
+        The number of times the knot passes through the hole in the
+        centre of the torus. Defaults to 3.
+    num_points : int
+        The number of points in the returned piecewise linear
+        curve. If there are multiple curves (i.e. a torus link), this
+        is the number of points in *each* curve.  Defaults to 100.
+    major_radius : float
+        Distance from the center of the torus tube to the center of the torus.
+        Defaults to 10.
+    minor_radius : float
+        The radius of the torus tube. Defaults to 5.
+
+    """
+
+    def __init__(self, p=3, q=2, num_points=100, major_radius=10.,
+                 minor_radius=5.):
+        self._p = p
+        self._q = q
+        self._num_points = num_points
+        self._major_radius = major_radius
+        self._minor_radius = minor_radius
+
+        self._calculate_vertices()
+
+    def _calculate_vertices(self):
+        angles = np.linspace(0, 2*np.pi, self._num_points)
+
+        num_components = self.num_components
+
+        divisions = (np.max([self._q, self._p]) *
+                     np.min([self._q, self._p]) / self.num_components)
+        starting_angles = np.linspace(
+            0, 2*np.pi, divisions + 1)[
+            :num_components]
+        q = self._q / num_components
+        p = self._p / num_components
+
+        components = []
+        for starting_angle in starting_angles:
+            vertices = np.zeros((self._num_points, 3))
+            local_angles = angles + starting_angle
+            radii = (self._minor_radius * np.cos(q * angles) +
+                     self._major_radius)
+            vertices[:, 0] = radii * np.cos(p * local_angles)
+            vertices[:, 1] = radii * np.sin(p * local_angles)
+            vertices[:, 2] = (self._minor_radius * -1 *
+                              np.sin(q * angles))
+            components.append(vertices)
+
+        self._components = components
+
+    @property
+    def first_component(self):
+        '''The vertices of the first component line of the torus knot or link.
+        '''
+        return self._components[0]
+
+    @property
+    def components(self):
+        '''A list of the vertices in each line of the torus knot or link.
+        Even if p and q are coprime, this is a list with just one
+        entry.
+        '''
+        return self._components
+
+    @property
+    def num_components(self):
+        '''The number of component lines in the torus link. This is equal
+        to the greatest common divisor of p and q.
+        '''
+        return gcd(self._p, self._q)
+
+    @property
+    def q(self):
+        '''The q parameter of the torus knot or link.'''
+        return self._q
+
+    @q.setter
+    def q(self, q):
+        self._q = q
+        self._calculate_vertices()
+
+    @property
+    def p(self):
+        '''The p parameter of the torus knot or link.'''
+        return self._p
+
+    @p.setter
+    def p(self, p):
+        self._p = p
+        self._calculate_vertices()
+
+    @property
+    def minor_radius(self):
+        '''The minor radius of the torus.'''
+        return self._minor_radius
+
+    @minor_radius.setter
+    def minor_radius(self, r):
+        self._minor_radius = r
+        self._calculate_vertices()
+
+    @property
+    def major_radius(self):
+        '''The major radius of the torus.'''
+        return self._major_radius
+
+    @major_radius.setter
+    def major_radius(self, r):
+        self._major_radius = r
+        self._calculate_vertices()
+
+    @property
+    def num_points(self):
+        '''The number of points in the vertices returned for each knot/link
+        component'''
+        return self._num_points
+
+    @num_points.setter
+    def num_points(self, r):
+        self._num_points = r
+        self._calculate_vertices()
diff --git a/vispy/geometry/triangulation.py b/vispy/geometry/triangulation.py
index e5d4d13..ff47a38 100644
--- a/vispy/geometry/triangulation.py
+++ b/vispy/geometry/triangulation.py
@@ -1,18 +1,29 @@
 # -*- coding: utf8 -*-
+
 from __future__ import division, print_function
+import sys
 
+from itertools import permutations
 import numpy as np
+
 from ..ext.ordereddict import OrderedDict
-from itertools import permutations
+
+try:
+    # Try to use the C++ triangle library, faster than the
+    # pure Python version.
+    # The latest stable release only works with Python 2. The GitHub version
+    # works on Python 3 though, but the release has yet to be done.
+    import triangle
+    assert sys.version_info.major == 2
+    _TRIANGLE_AVAILABLE = True
+except (ImportError, AssertionError):
+    _TRIANGLE_AVAILABLE = False
 
 
 class Triangulation(object):
     """Constrained delaunay triangulation
 
-    Implementation based on:
-
-        * Domiter, V. and Žalik, B. Sweep‐line algorithm for constrained
-          Delaunay triangulation
+    Implementation based on [1]_.
 
     Parameters
     ----------
@@ -27,6 +38,13 @@ class Triangulation(object):
       triangulation, but adding legalisation would produce fewer thin
       triangles.
     * The pts and edges arrays may be modified.
+
+    References
+    ----------
+    .. [1] Domiter, V. and Žalik, B. Sweep‐line algorithm for constrained
+       Delaunay triangulation
+
+
     """
     def __init__(self, pts, edges):
         self.pts = pts[:, :2].astype(np.float32)
@@ -37,26 +55,26 @@ class Triangulation(object):
             raise TypeError('edges argument must be ndarray of shape (N, 2).')
         
         # described in initialize()
-        self.front = None
+        self._front = None
         self.tris = OrderedDict()
-        self.edges_lookup = {}
+        self._edges_lookup = {}
         
-    def normalize(self):
+    def _normalize(self):
         # Clean up data   (not discussed in original publication)
         
         # (i) Split intersecting edges. Every edge that intersects another 
         #     edge or point is split. This extends self.pts and self.edges.
-        self.split_intersecting_edges()
+        self._split_intersecting_edges()
         
         # (ii) Merge identical points. If any two points are found to be equal,
         #      the second is removed and the edge table is updated accordingly. 
-        self.merge_duplicate_points()
+        self._merge_duplicate_points()
 
         # (iii) Remove duplicate edges
         # TODO
 
-    def initialize(self):
-        self.normalize()
+    def _initialize(self):
+        self._normalize()
         ## Initialization (sec. 3.3)
 
         # sort points by y, then x
@@ -89,12 +107,12 @@ class Triangulation(object):
         self.edges += 2
 
         # find topmost point in each edge
-        self.tops = self.edges.max(axis=1)
-        self.bottoms = self.edges.min(axis=1)
+        self._tops = self.edges.max(axis=1)
+        self._bottoms = self.edges.min(axis=1)
 
         # inintialize sweep front
         # values in this list are indexes into self.pts
-        self.front = [0, 2, 1]
+        self._front = [0, 2, 1]
         
         # empty triangle list. 
         # This will contain [(a, b, c), ...] where a,b,c are indexes into 
@@ -105,13 +123,15 @@ class Triangulation(object):
         # This is used to look up the thrid point in a triangle, given any 
         # edge. Since each edge has two triangles, they are independently 
         # stored as (a, b): c and (b, a): d
-        self.edges_lookup = {}
+        self._edges_lookup = {}
 
     def triangulate(self):
-        self.initialize()
+        """Do the triangulation
+        """
+        self._initialize()
         
         pts = self.pts
-        front = self.front
+        front = self._front
         
         ## Begin sweep (sec. 3.4)
         for i in range(3, pts.shape[0]):
@@ -132,14 +152,14 @@ class Triangulation(object):
             if pi[0] > pl[0]:  
                 #debug("  mid case")
                 # Add a single triangle connecting pi,pl,pr
-                self.add_tri(front[l], front[l+1], i)
+                self._add_tri(front[l], front[l+1], i)
                 front.insert(l+1, i)
             # "(ii) left case"
             else:
                 #debug("  left case")
                 # Add triangles connecting pi,pl,ps and pi,pl,pr
-                self.add_tri(front[l], front[l+1], i)
-                self.add_tri(front[l-1], front[l], i)
+                self._add_tri(front[l], front[l+1], i)
+                self._add_tri(front[l-1], front[l], i)
                 front[l] = i
             
             #debug(front)
@@ -162,7 +182,7 @@ class Triangulation(object):
                     err = np.geterr()
                     np.seterr(invalid='ignore')
                     try:
-                        angle = np.arccos(self.cosine(pi, p1, p2))
+                        angle = np.arccos(self._cosine(pi, p1, p2))
                     finally:
                         np.seterr(**err)
                     
@@ -174,42 +194,43 @@ class Triangulation(object):
                     assert (i != front[ind1] and 
                             front[ind1] != front[ind2] and 
                             front[ind2] != i)
-                    self.add_tri(i, front[ind1], front[ind2], source='smooth1')
+                    self._add_tri(i, front[ind1], front[ind2],
+                                  source='smooth1')
                     front.pop(ind1)
             #debug("Finished smoothing front.")
             
             # "edge event" (sec. 3.4.2)
             # remove any triangles cut by completed edges and re-fill 
             # the holes.
-            if i in self.tops:
-                for j in self.bottoms[self.tops == i]:
+            if i in self._tops:
+                for j in self._bottoms[self._tops == i]:
                     # Make sure edge (j, i) is present in mesh
                     # because edge event may have created a new front list
-                    self.edge_event(i, j)  
-                    front = self.front 
+                    self._edge_event(i, j)  
+                    front = self._front 
                 
-        self.finalize()
+        self._finalize()
         
         self.tris = np.array(list(self.tris.keys()), dtype=int)
         
         #debug("Finished with %d tris:" % self.tris.shape[0])
         #debug(str(self.tris))
         
-    def finalize(self):
+    def _finalize(self):
         ## Finalize (sec. 3.5)
 
         # (i) Add bordering triangles to fill hull
         #debug("== Fill hull")
-        front = list(OrderedDict.fromkeys(self.front))
+        front = list(OrderedDict.fromkeys(self._front))
 
         l = len(front) - 2
         k = 1
         while k < l-1:
             # if edges lie in counterclockwise direction, then signed area 
             # is positive
-            if self.iscounterclockwise(front[k], front[k+1], front[k+2]):
-                self.add_tri(front[k], front[k+1], front[k+2], legal=False, 
-                             source='fill_hull')
+            if self._iscounterclockwise(front[k], front[k+1], front[k+2]):
+                self._add_tri(front[k], front[k+1], front[k+2], legal=False, 
+                              source='fill_hull')
                 front.pop(k+1)
                 l -= 1
                 continue
@@ -237,14 +258,14 @@ class Triangulation(object):
                 for i in (0, 1, 2):
                     edge = (t[i], t[(i + 1) % 3])
                     pt = t[(i + 2) % 3]
-                    t2 = self.adjacent_tri(edge, pt)
+                    t2 = self._adjacent_tri(edge, pt)
                     if t2 is None:
                         continue
                     t2a = t2[1:3] + t2[0:1]
                     t2b = t2[2:3] + t2[0:2]
                     if t2 in tri_state or t2a in tri_state or t2b in tri_state:
                         continue
-                    if self.is_constraining_edge(edge):
+                    if self._is_constraining_edge(edge):
                         tri_state[t2] = 1 - v
                     else:
                         tri_state[t2] = v
@@ -253,22 +274,22 @@ class Triangulation(object):
         
         for t, v in tri_state.items():
             if v == 0:
-                self.remove_tri(*t)
+                self._remove_tri(*t)
 
-    def edge_event(self, i, j):
+    def _edge_event(self, i, j):
         """
         Force edge (i, j) to be present in mesh. 
         This works by removing intersected triangles and filling holes up to
         the cutting edge.
         """
-        front_index = self.front.index(i)
+        front_index = self._front.index(i)
         
         #debug("  == edge event ==")
-        front = self.front
+        front = self._front
 
         # First just see whether this edge is already present
         # (this is not in the published algorithm)
-        if (i, j) in self.edges_lookup or (j, i) in self.edges_lookup:
+        if (i, j) in self._edges_lookup or (j, i) in self._edges_lookup:
             #debug("    already added.")
             return
         #debug("    Edge (%d,%d) not added yet. Do edge event. (%s - %s)" % 
@@ -299,13 +320,13 @@ class Triangulation(object):
         front_dir = 1 if self.pts[j][0] > self.pts[i][0] else -1
                 
         # Initialize search state
-        if self.edge_below_front((i, j), front_index):
+        if self._edge_below_front((i, j), front_index):
             mode = 1  # follow triangles
-            tri = self.find_cut_triangle((i, j))
-            last_edge = self.edge_opposite_point(tri, i)
-            next_tri = self.adjacent_tri(last_edge, i)
+            tri = self._find_cut_triangle((i, j))
+            last_edge = self._edge_opposite_point(tri, i)
+            next_tri = self._adjacent_tri(last_edge, i)
             assert next_tri is not None
-            self.remove_tri(*tri)
+            self._remove_tri(*tri)
             # todo: does this work? can we count on last_edge to be clockwise
             # around point i?
             lower_polygon.append(last_edge[1])
@@ -333,20 +354,20 @@ class Triangulation(object):
                     lower_polygon.append(j)
                     #debug("    Appended to upper_polygon:", upper_polygon)
                     #debug("    Appended to lower_polygon:", lower_polygon)
-                    self.remove_tri(*next_tri)
+                    self._remove_tri(*next_tri)
                     break
                 else:
                     # next triangle does not contain the end point; we will
                     # cut one of the two far edges.
-                    tri_edges = self.edges_in_tri_except(next_tri, last_edge)
+                    tri_edges = self._edges_in_tri_except(next_tri, last_edge)
                     
                     # select the edge that is cut
-                    last_edge = self.intersected_edge(tri_edges, (i, j))
+                    last_edge = self._intersected_edge(tri_edges, (i, j))
                     #debug("    set last_edge to intersected edge:", last_edge)
                     last_tri = next_tri
-                    next_tri = self.adjacent_tri(last_edge, last_tri)
+                    next_tri = self._adjacent_tri(last_edge, last_tri)
                     #debug("    set next_tri:", next_tri)
-                    self.remove_tri(*last_tri)
+                    self._remove_tri(*last_tri)
 
                     # Crossing an edge adds one point to one of the polygons
                     if lower_polygon[-1] == last_edge[0]:
@@ -365,7 +386,7 @@ class Triangulation(object):
                         raise RuntimeError("Something went wrong..")
                     
                     # If we crossed the front, go to mode 2
-                    x = self.edge_in_front(last_edge)
+                    x = self._edge_in_front(last_edge)
                     if x >= 0:  # crossing over front
                         #debug("    -> crossed over front, prepare for mode 2")
                         mode = 2
@@ -425,7 +446,7 @@ class Triangulation(object):
                 front_holes.append(front_index)
                 #debug("    Append to front_holes:", front_holes)
 
-                if self.edges_intersect((i, j), next_edge):
+                if self._edges_intersect((i, j), next_edge):
                     # crossing over front into triangle
                     #debug("    -> crossed over front, prepare for mode 1")
                     mode = 1
@@ -435,7 +456,7 @@ class Triangulation(object):
                     
                     # we are crossing the front, so this edge only has one
                     # triangle. 
-                    next_tri = self.tri_from_edge(last_edge)
+                    next_tri = self._tri_from_edge(last_edge)
                     #debug("    Set next_tri:", next_tri)
                     
                     upper_polygon.append(front[front_index+front_dir])
@@ -452,14 +473,14 @@ class Triangulation(object):
         
         #debug("Filling edge_event polygons...")
         for polygon in [lower_polygon, upper_polygon]:
-            dist = self.distances_from_line((i, j), polygon)
+            dist = self._distances_from_line((i, j), polygon)
             #debug("Distances:", dist)
             while len(polygon) > 2:
                 ind = np.argmax(dist)
                 #debug("Next index: %d" % ind)
-                self.add_tri(polygon[ind], polygon[ind-1],
-                             polygon[ind+1], legal=False, 
-                             source='edge_event')
+                self._add_tri(polygon[ind], polygon[ind-1],
+                              polygon[ind+1], legal=False, 
+                              source='edge_event')
                 polygon.pop(ind)
                 dist.pop(ind)
 
@@ -473,7 +494,7 @@ class Triangulation(object):
 
         #debug("Finished updating front after edge_event.")
         
-    def find_cut_triangle(self, edge):
+    def _find_cut_triangle(self, edge):
         """
         Return the triangle that has edge[0] as one of its vertices and is 
         bisected by edge.
@@ -483,11 +504,11 @@ class Triangulation(object):
         edges = []  # opposite edge for each triangle attached to edge[0]
         for tri in self.tris:
             if edge[0] in tri:
-                edges.append(self.edge_opposite_point(tri, edge[0]))
+                edges.append(self._edge_opposite_point(tri, edge[0]))
                 
         for oedge in edges:
-            o1 = self.orientation(edge, oedge[0])
-            o2 = self.orientation(edge, oedge[1]) 
+            o1 = self._orientation(edge, oedge[0])
+            o2 = self._orientation(edge, oedge[1]) 
             #debug(edge, oedge, o1, o2)
             #debug(self.pts[np.array(edge)])
             #debug(self.pts[np.array(oedge)])
@@ -496,24 +517,24 @@ class Triangulation(object):
         
         return None
 
-    def edge_in_front(self, edge):
+    def _edge_in_front(self, edge):
         """ Return the index where *edge* appears in the current front.
         If the edge is not in the front, return -1
         """
         e = (list(edge), list(edge)[::-1])
-        for i in range(len(self.front)-1):
-            if self.front[i:i+2] in e:
+        for i in range(len(self._front)-1):
+            if self._front[i:i+2] in e:
                 return i
         return -1
 
-    def edge_opposite_point(self, tri, i):
+    def _edge_opposite_point(self, tri, i):
         """ Given a triangle, return the edge that is opposite point i.
         Vertexes are returned in the same orientation as in tri.
         """
         ind = tri.index(i)
         return (tri[(ind+1) % 3], tri[(ind+2) % 3])
 
-    def adjacent_tri(self, edge, i):
+    def _adjacent_tri(self, edge, i):
         """
         Given a triangle formed by edge and i, return the triangle that shares
         edge. *i* may be either a point or the entire triangle.
@@ -522,8 +543,8 @@ class Triangulation(object):
             i = [x for x in i if x not in edge][0]
 
         try:
-            pt1 = self.edges_lookup[edge]
-            pt2 = self.edges_lookup[(edge[1], edge[0])]
+            pt1 = self._edges_lookup[edge]
+            pt2 = self._edges_lookup[(edge[1], edge[0])]
         except KeyError:
             return None
             
@@ -535,13 +556,13 @@ class Triangulation(object):
             raise RuntimeError("Edge %s and point %d do not form a triangle "
                                "in this mesh." % (edge, i))
 
-    def tri_from_edge(self, edge):
+    def _tri_from_edge(self, edge):
         """Return the only tri that contains *edge*. If two tris share this
         edge, raise an exception.
         """
         edge = tuple(edge)
-        p1 = self.edges_lookup.get(edge, None)
-        p2 = self.edges_lookup.get(edge[::-1], None)
+        p1 = self._edges_lookup.get(edge, None)
+        p2 = self._edges_lookup.get(edge[::-1], None)
         if p1 is None:
             if p2 is None:
                 raise RuntimeError("No tris connected to edge %r" % (edge,))
@@ -551,7 +572,7 @@ class Triangulation(object):
         else:
             raise RuntimeError("Two triangles connected to edge %r" % (edge,))
 
-    def edges_in_tri_except(self, tri, edge):
+    def _edges_in_tri_except(self, tri, edge):
         """Return the edges in *tri*, excluding *edge*.
         """
         edges = [(tri[i], tri[(i+1) % 3]) for i in range(3)]
@@ -561,31 +582,31 @@ class Triangulation(object):
             edges.remove(tuple(edge[::-1]))
         return edges
 
-    def edge_below_front(self, edge, front_index):
+    def _edge_below_front(self, edge, front_index):
         """Return True if *edge* is below the current front. 
         
         One of the points in *edge* must be _on_ the front, at *front_index*.
         """
-        f0 = self.front[front_index-1]
-        f1 = self.front[front_index+1]
-        return (self.orientation(edge, f0) > 0 and 
-                self.orientation(edge, f1) < 0)
+        f0 = self._front[front_index-1]
+        f1 = self._front[front_index+1]
+        return (self._orientation(edge, f0) > 0 and 
+                self._orientation(edge, f1) < 0)
 
-    def is_constraining_edge(self, edge):
+    def _is_constraining_edge(self, edge):
         mask1 = self.edges == edge[0]
         mask2 = self.edges == edge[1]
         return (np.any(mask1[:, 0] & mask2[:, 1]) or 
                 np.any(mask2[:, 0] & mask1[:, 1]))
     
-    def intersected_edge(self, edges, cut_edge):
+    def _intersected_edge(self, edges, cut_edge):
         """ Given a list of *edges*, return the first that is intersected by
         *cut_edge*.
         """
         for edge in edges:
-            if self.edges_intersect(edge, cut_edge):
+            if self._edges_intersect(edge, cut_edge):
                 return edge
 
-    def find_edge_intersections(self):
+    def _find_edge_intersections(self):
         """
         Return a dictionary containing, for each edge in self.edges, a list
         of the positions at which the edge should be split.
@@ -594,9 +615,9 @@ class Triangulation(object):
         cuts = {}  # { edge: [(intercept, point), ...], ... }
         for i in range(edges.shape[0]-1):
             # intersection of edge i onto all others
-            int1 = self.intersect_edge_arrays(edges[i:i+1], edges[i+1:])
+            int1 = self._intersect_edge_arrays(edges[i:i+1], edges[i+1:])
             # intersection of all edges onto edge i
-            int2 = self.intersect_edge_arrays(edges[i+1:], edges[i:i+1])
+            int2 = self._intersect_edge_arrays(edges[i+1:], edges[i:i+1])
         
             # select for pairs that intersect
             err = np.geterr()
@@ -633,14 +654,14 @@ class Triangulation(object):
                     v.pop(i+1)
         return cuts
 
-    def split_intersecting_edges(self):
+    def _split_intersecting_edges(self):
         # we can do all intersections at once, but this has excessive memory
         # overhead.
-        #int1 = self.intersection_matrix(edges)
+        #int1 = self._intersection_matrix(edges)
         #int2 = int1.T
         
         # measure intersection point between all pairs of edges
-        all_cuts = self.find_edge_intersections()
+        all_cuts = self._find_edge_intersections()
 
         # cut edges at each intersection
         add_pts = []
@@ -679,7 +700,7 @@ class Triangulation(object):
             add_edges = np.array(add_edges, dtype=self.edges.dtype)
             self.edges = np.append(self.edges, add_edges, axis=0)
 
-    def merge_duplicate_points(self):
+    def _merge_duplicate_points(self):
         # generate a list of all pairs (i,j) of identical points
         dups = []
         for i in range(self.pts.shape[0]-1):
@@ -717,13 +738,13 @@ class Triangulation(object):
         mask = self.edges[:, 0] != self.edges[:, 1]
         self.edges = self.edges[mask]
     
-    def distance(self, A, B):
+    def _distance(self, A, B):
         # Distance between points A and B
         n = len(A)
         assert len(B) == n
         return np.linalg.norm(np.array(list(A)) - np.array(list(B)))
 
-    def distances_from_line(self, edge, points):
+    def _distances_from_line(self, edge, points):
         # Distance of a set of points from a given line
         #debug("distance from %r to %r" % (points, edge))
         e1 = self.pts[edge[0]]
@@ -731,12 +752,12 @@ class Triangulation(object):
         distances = []
         for i in points:
             p = self.pts[i]
-            proj = self.projection(e1, p, e2)
+            proj = self._projection(e1, p, e2)
             distances.append(((p - proj)**2).sum()**0.5)
         assert distances[0] == 0 and distances[-1] == 0
         return distances
 
-    def projection(self, a, b, c):
+    def _projection(self, a, b, c):
         """Return projection of (a,b) onto (a,c)
         Arguments are point locations, not indexes.
         """
@@ -744,7 +765,7 @@ class Triangulation(object):
         ac = c - a
         return a + ((ab*ac).sum() / (ac*ac).sum()) * ac
 
-    def cosine(self, A, B, C):
+    def _cosine(self, A, B, C):
         # Cosine of angle ABC
         a = ((C - B)**2).sum()
         b = ((C - A)**2).sum()
@@ -752,7 +773,7 @@ class Triangulation(object):
         d = (a + c - b) / ((4 * a * c)**0.5)
         return d
 
-    #def barycentric(self, A, B, C, p, q, r):
+    #def _barycentric(self, A, B, C, p, q, r):
         ## Cartesian coordinates of the point whose barycentric coordinates
         ## with respect to the triangle ABC are [p,q,r]
         #n = len(A)
@@ -761,7 +782,7 @@ class Triangulation(object):
         #p, q, r = p/s, q/s, r/s
         #return tuple([p*A[i]+q*B[i]+r*C[i] for i in range(n)])
 
-    #def trilinear(self, A, B, C, alpha, beta, gamma):
+    #def _trilinear(self, A, B, C, alpha, beta, gamma):
         ## Cartesian coordinates of the point whose trilinear coordinates
         ## with respect to the triangle ABC are [alpha,beta,gamma]
         #a = distance(B, C)
@@ -769,7 +790,7 @@ class Triangulation(object):
         #c = distance(A, B)
         #return barycentric(A, B, C, a*alpha, b*beta, c*gamma)
                 
-    #def circuminfo(self, A, B, C):
+    #def _circuminfo(self, A, B, C):
         ## Cartesian coordinates of the circumcenter of triangle ABC
         #cosA = cosine(C, A, B)
         #cosB = cosine(A, B, C)
@@ -778,21 +799,21 @@ class Triangulation(object):
         ## returns circumcenter and circumradius
         #return cc, distance(cc, A)
 
-    def iscounterclockwise(self, a, b, c):
+    def _iscounterclockwise(self, a, b, c):
         # Check if the points lie in counter-clockwise order or not
         A = self.pts[a]
         B = self.pts[b]
         C = self.pts[c]
         return np.cross(B-A, C-B) > 0
 
-    def edges_intersect(self, edge1, edge2):
+    def _edges_intersect(self, edge1, edge2):
         """
         Return 1 if edges intersect completely (endpoints excluded)
         """
-        h12 = self.intersect_edge_arrays(self.pts[np.array(edge1)], 
-                                         self.pts[np.array(edge2)])
-        h21 = self.intersect_edge_arrays(self.pts[np.array(edge2)], 
-                                         self.pts[np.array(edge1)])
+        h12 = self._intersect_edge_arrays(self.pts[np.array(edge1)], 
+                                          self.pts[np.array(edge2)])
+        h21 = self._intersect_edge_arrays(self.pts[np.array(edge2)], 
+                                          self.pts[np.array(edge1)])
         err = np.geterr()
         np.seterr(divide='ignore', invalid='ignore')
         try:
@@ -801,7 +822,7 @@ class Triangulation(object):
             np.seterr(**err)
         return out
 
-    def intersection_matrix(self, lines):
+    def _intersection_matrix(self, lines):
         """
         Return a 2D array of intercepts such that 
         intercepts[i, j] is the intercept of lines[i] onto lines[j].
@@ -811,10 +832,10 @@ class Triangulation(object):
         
         The intercept is described in intersect_edge_arrays().
         """
-        return self.intersect_edge_arrays(lines[:, np.newaxis, ...], 
-                                          lines[np.newaxis, ...])
+        return self._intersect_edge_arrays(lines[:, np.newaxis, ...], 
+                                           lines[np.newaxis, ...])
         
-    def intersect_edge_arrays(self, lines1, lines2):
+    def _intersect_edge_arrays(self, lines1, lines2):
         """Return the intercepts of all lines defined in *lines1* as they 
         intersect all lines in *lines2*. 
         
@@ -856,7 +877,7 @@ class Triangulation(object):
         
         return h
 
-    def orientation(self, edge, point):
+    def _orientation(self, edge, point):
         """ Returns +1 if edge[0]->point is clockwise from edge[0]->edge[1], 
         -1 if counterclockwise, and 0 if parallel.
         """
@@ -865,7 +886,7 @@ class Triangulation(object):
         c = np.cross(v1, v2)  # positive if v1 is CW from v2
         return 1 if c > 0 else (-1 if c < 0 else 0)
 
-    #def legalize(self, p):
+    #def _legalize(self, p):
         ### Legalize recursively - incomplete
         #return p  # disabled for now
     
@@ -886,7 +907,7 @@ class Triangulation(object):
 
         #return (f00, f11, p)
 
-    def add_tri(self, a, b, c, legal=True, source=None):
+    def _add_tri(self, a, b, c, legal=True, source=None):
         # source is just used for #debugging
         #debug("Add triangle [%s]:" % source, (a, b, c))
         
@@ -908,31 +929,31 @@ class Triangulation(object):
                                 ((a, b, c), t))
         
         # TODO: should add to edges_lookup after legalization??
-        if self.iscounterclockwise(a, b, c):
+        if self._iscounterclockwise(a, b, c):
             #debug("    ", (a, b), (b, c), (c, a))
-            assert (a, b) not in self.edges_lookup
-            assert (b, c) not in self.edges_lookup
-            assert (c, a) not in self.edges_lookup
-            self.edges_lookup[(a, b)] = c
-            self.edges_lookup[(b, c)] = a
-            self.edges_lookup[(c, a)] = b
+            assert (a, b) not in self._edges_lookup
+            assert (b, c) not in self._edges_lookup
+            assert (c, a) not in self._edges_lookup
+            self._edges_lookup[(a, b)] = c
+            self._edges_lookup[(b, c)] = a
+            self._edges_lookup[(c, a)] = b
         else:
             #debug("    ", (b, a), (c, b), (a, c))
-            assert (b, a) not in self.edges_lookup
-            assert (c, b) not in self.edges_lookup
-            assert (a, c) not in self.edges_lookup
-            self.edges_lookup[(b, a)] = c
-            self.edges_lookup[(c, b)] = a
-            self.edges_lookup[(a, c)] = b
+            assert (b, a) not in self._edges_lookup
+            assert (c, b) not in self._edges_lookup
+            assert (a, c) not in self._edges_lookup
+            self._edges_lookup[(b, a)] = c
+            self._edges_lookup[(c, b)] = a
+            self._edges_lookup[(a, c)] = b
         
         #if legal:
-            #tri = self.legalize((a, b, c))
+            #tri = self._legalize((a, b, c))
         #else:
         tri = (a, b, c)
         
         self.tris[tri] = None
 
-    def remove_tri(self, a, b, c):
+    def _remove_tri(self, a, b, c):
         #debug("Remove triangle:", (a, b, c))
         
         for k in permutations((a, b, c)):
@@ -941,16 +962,16 @@ class Triangulation(object):
         del self.tris[k]
         (a, b, c) = k
 
-        if self.edges_lookup.get((a, b), -1) == c:
+        if self._edges_lookup.get((a, b), -1) == c:
             #debug("    ", (a,b), (b,c), (c,a))
-            del self.edges_lookup[(a, b)]
-            del self.edges_lookup[(b, c)]
-            del self.edges_lookup[(c, a)]
-        elif self.edges_lookup.get((b, a), -1) == c:
+            del self._edges_lookup[(a, b)]
+            del self._edges_lookup[(b, c)]
+            del self._edges_lookup[(c, a)]
+        elif self._edges_lookup.get((b, a), -1) == c:
             #debug("    ", (b,a), (c,b), (a,c))
-            del self.edges_lookup[(b, a)]
-            del self.edges_lookup[(a, c)]
-            del self.edges_lookup[(c, b)]
+            del self._edges_lookup[(b, a)]
+            del self._edges_lookup[(a, c)]
+            del self._edges_lookup[(c, b)]
         else:
             raise RuntimeError("Lost edges_lookup for tri (%d, %d, %d)" % 
                                (a, b, c))
@@ -958,7 +979,57 @@ class Triangulation(object):
         return k
 
 
-# Note: using custom #debug instead of logging because 
+def _triangulate_python(vertices_2d, segments):
+    segments = segments.reshape(len(segments) / 2, 2)
+    T = Triangulation(vertices_2d, segments)
+    T.triangulate()
+    vertices_2d = T.pts
+    triangles = T.tris.ravel()
+    return vertices_2d, triangles
+
+
+def _triangulate_cpp(vertices_2d, segments):
+    T = triangle.triangulate({'vertices': vertices_2d,
+                              'segments': segments}, "p")
+    vertices_2d = T["vertices"]
+    triangles = T["triangles"]
+    return vertices_2d, triangles
+
+
+def triangulate(vertices):
+    """Triangulate a set of vertices
+
+    Parameters
+    ----------
+    vertices : array-like
+        The vertices.
+
+    Returns
+    -------
+    vertices : array-like
+        The vertices.
+    tringles : array-like
+        The triangles.
+    """
+    n = len(vertices)
+    vertices = np.asarray(vertices)
+    zmean = vertices[:, 2].mean()
+    vertices_2d = vertices[:, :2]
+    segments = np.repeat(np.arange(n + 1), 2)[1:-1]
+    segments[-2:] = n - 1, 0
+
+    if _TRIANGLE_AVAILABLE:
+        vertices_2d, triangles = _triangulate_cpp(vertices_2d, segments)
+    else:
+        vertices_2d, triangles = _triangulate_python(vertices_2d, segments)
+
+    vertices = np.empty((len(vertices_2d), 3))
+    vertices[:, :2] = vertices_2d
+    vertices[:, 2] = zmean
+    return vertices, triangles
+
+
+# Note: using custom #debug instead of logging because
 # there are MANY messages and logger might be too expensive.
 # After this becomes stable, we might just remove them altogether.
 def debug(*args):
diff --git a/vispy/gloo/__init__.py b/vispy/gloo/__init__.py
index 1901091..21fb092 100644
--- a/vispy/gloo/__init__.py
+++ b/vispy/gloo/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -13,7 +13,7 @@ This set of classes provides a friendly (Pythonic) interface
 to OpenGL, and is designed to provide OpenGL's full functionality.
 
 All classes inherit from GLObject, which provide a basic interface,
-enabling activatinge and deleting the object. Central to each
+enabling, activating and deleting the object. Central to each
 visualization is the Program. Other objects, such as Texture2D and
 VertexBuffer should be set as uniforms and attributes of the Program
 object.
@@ -37,9 +37,7 @@ Example::
     progress and there are still a few known limitations. Most notably:
 
     * TextureCubeMap is not yet implemented
-    * FBO's can only do 2D textures (not 3D textures or cube maps)
-    * Sharing of Shaders and RenderBuffers (between multiple Program's and
-      FrameBuffers, respectively) is not well supported.
+    * FBOs can only do 2D textures (not 3D textures or cube maps)
     * No support for compressed textures.
 
 """
@@ -47,12 +45,12 @@ Example::
 from __future__ import division
 
 from . import gl  # noqa
+from .wrappers import *  # noqa
+from .context import (GLContext, get_default_config,  # noqa
+                      get_current_canvas)  # noqa
 from .globject import GLObject  # noqa
 from .buffer import VertexBuffer, IndexBuffer  # noqa
-from .initialize import gl_initialize  # noqa
-from .texture import Texture2D, TextureAtlas, Texture3D  # noqa
-from .shader import VertexShader, FragmentShader  # noqa
+from .texture import Texture1D, Texture2D, TextureAtlas, Texture3D, TextureEmulated3D  # noqa
 from .program import Program  # noqa
-from .framebuffer import (FrameBuffer, ColorBuffer, DepthBuffer,  # noqa
-                          StencilBuffer)  # noqa
-from .wrappers import *  # noqa
+from .framebuffer import FrameBuffer, RenderBuffer  # noqa
+from . import util  # noqa
diff --git a/vispy/gloo/buffer.py b/vispy/gloo/buffer.py
index dd49e5d..7904bf8 100644
--- a/vispy/gloo/buffer.py
+++ b/vispy/gloo/buffer.py
@@ -1,16 +1,17 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
-import sys
-
 import numpy as np
+from os import path as op
+from traceback import extract_stack, format_list
+import weakref
 
-from . import gl
 from . globject import GLObject
 from ..util import logger
+from ..ext.six import string_types
 
 
 # ------------------------------------------------------------ Buffer class ---
@@ -18,7 +19,7 @@ class Buffer(GLObject):
     """ Generic GPU buffer.
 
     A generic buffer is an interface used to upload data to a GPU array buffer
-    (gl.GL_ARRAY_BUFFER or gl.GL_ELEMENT_ARRAY_BUFFER). It keeps track of
+    (ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER). It keeps track of
     buffer size but does not have any CPU storage. You can consider it as
     write-only.
 
@@ -31,54 +32,35 @@ class Buffer(GLObject):
 
     Parameters
     ----------
-    target : GLenum
-        gl.GL_ARRAY_BUFFER or gl.GL_ELEMENT_ARRAY_BUFFER
-    data : ndarray
-        Buffer data
-    nbytes : int
-        Buffer byte size
+    data : ndarray | None
+        Buffer data.
+    nbytes : int | None
+        Buffer byte size.
     """
-
-    def __init__(self, data=None, target=gl.GL_ARRAY_BUFFER, nbytes=None):
-
+    
+    def __init__(self, data=None, nbytes=None):
         GLObject.__init__(self)
-        self._views = []
-        self._valid = True
-
-        # For ATI bug
-        self._bufferSubDataOk = False
-
-        # Store and check target
-        if target not in (gl.GL_ARRAY_BUFFER, gl.GL_ELEMENT_ARRAY_BUFFER):
-            raise ValueError("Invalid target for buffer object")
-        self._target = target
-
-        # Bytesize of buffer in GPU memory
-        self._buffer_size = None
-        # Bytesize of buffer in CPU memory
-        self._nbytes = 0
-
-        # Buffer usage (GL_STATIC_DRAW, G_STREAM_DRAW or GL_DYNAMIC_DRAW)
-        self._usage = gl.GL_DYNAMIC_DRAW
-
+        self._views = []  # Views on this buffer (stored using weakrefs)
+        self._valid = True  # To invalidate buffer views
+        self._nbytes = 0  # Bytesize in bytes, set in resize_bytes()
+        
         # Set data
-        self._pending_data = []
         if data is not None:
             if nbytes is not None:
                 raise ValueError("Cannot specify both data and nbytes.")
             self.set_data(data, copy=False)
         elif nbytes is not None:
-            self._nbytes = nbytes
+            self.resize_bytes(nbytes)
         
     @property
     def nbytes(self):
-        """ Buffer byte size """
+        """ Buffer size in bytes """
 
         return self._nbytes
-    
+
     def set_subdata(self, data, offset=0, copy=False):
         """ Set a sub-region of the buffer (deferred operation).
-        
+
         Parameters
         ----------
 
@@ -102,17 +84,16 @@ class Buffer(GLObject):
         # If the whole buffer is to be written, we clear any pending data
         # (because they will be overwritten anyway)
         if nbytes == self._nbytes and offset == 0:
-            self._pending_data = []
-        self._pending_data.append((data, nbytes, offset))
+            self._glir.command('SIZE', self._id, nbytes)
+        self._glir.command('DATA', self._id, offset, data)
 
     def set_data(self, data, copy=False):
         """ Set data in the buffer (deferred operation).
-        
+
         This completely resets the size and contents of the buffer.
 
         Parameters
         ----------
-
         data : ndarray
             Data to be uploaded
         copy: bool
@@ -125,10 +106,13 @@ class Buffer(GLObject):
 
         if nbytes != self._nbytes:
             self.resize_bytes(nbytes)
-
-        # We can discard any other pending operations here.
-        self._pending_data = [(data, nbytes, 0)]
-
+        else:
+            # Use SIZE to discard any previous data setting
+            self._glir.command('SIZE', self._id, nbytes)
+        
+        if nbytes:  # Only set data if there *is* data
+            self._glir.command('DATA', self._id, 0, data)
+    
     def resize_bytes(self, size):
         """ Resize this buffer (deferred operation). 
         
@@ -138,84 +122,13 @@ class Buffer(GLObject):
             New buffer size in bytes.
         """
         self._nbytes = size
-        self._pending_data = []
+        self._glir.command('SIZE', self._id, size)
         # Invalidate any view on this buffer
         for view in self._views:
-            view._valid = False
+            if view() is not None:
+                view()._valid = False
         self._views = []
 
-    def _create(self):
-        """ Create buffer on GPU """
-
-        logger.debug("GPU: Creating buffer")
-        self._handle = gl.glCreateBuffer()
-
-    def _delete(self):
-        """ Delete buffer from GPU """
-
-        logger.debug("GPU: Deleting buffer")
-        gl.glDeleteBuffer(self._handle)
-
-    def _resize_bytes(self):
-        """ """
-
-        logger.debug("GPU: Resizing buffer(%d bytes)" % self._nbytes)
-        gl.glBufferData(self._target, self._nbytes, self._usage)
-        self._buffer_size = self._nbytes
-
-    def _activate(self):
-        """ Bind the buffer to some target """
-
-        logger.debug("GPU: Activating buffer")
-        gl.glBindBuffer(self._target, self._handle)
-        
-        # Resize if necessary
-        if self._buffer_size != self._nbytes:
-            self._resize_bytes()
-        
-        # Update pending data if necessary
-        if self._pending_data:
-            logger.debug("GPU: Updating buffer (%d pending operation(s))" %
-                         len(self._pending_data))
-            self._update_data()
-    
-    def _deactivate(self):
-        """ Unbind the current bound buffer """
-
-        logger.debug("GPU: Deactivating buffer")
-        gl.glBindBuffer(self._target, 0)
-
-    def _update_data(self):
-        """ Upload all pending data to GPU. """
-
-        # Update data
-        while self._pending_data:
-            data, nbytes, offset = self._pending_data.pop(0)
-
-            # Determine whether to check errors to try handling the ATI bug
-            check_ati_bug = ((not self._bufferSubDataOk) and
-                             (gl.current_backend is gl.desktop) and
-                             sys.platform.startswith('win'))
-
-            # flush any pending errors
-            if check_ati_bug:
-                gl.check_error('periodic check')
-
-            try:
-                gl.glBufferSubData(self._target, offset, data)
-                if check_ati_bug:
-                    gl.check_error('glBufferSubData')
-                self._bufferSubDataOk = True  # glBufferSubData seems to work
-            except Exception:
-                # This might be due to a driver error (seen on ATI), issue #64.
-                # We try to detect this, and if we can use glBufferData instead
-                if offset == 0 and nbytes == self._nbytes:
-                    gl.glBufferData(self._target, data, self._usage)
-                    logger.debug("Using glBufferData instead of " +
-                                 "glBufferSubData (known ATI bug).")
-                else:
-                    raise
-
 
 # -------------------------------------------------------- DataBuffer class ---
 class DataBuffer(Buffer):
@@ -223,101 +136,60 @@ class DataBuffer(Buffer):
 
     Parameters
     ----------
-
-    target : GLENUM
-        gl.GL_ARRAY_BUFFER or gl.GL_ELEMENT_ARRAY_BUFFER
-    data : ndarray
-        Buffer data
-    dtype : dtype
-        Buffer data type
-    size : int
-        Number of elements in buffer
-    base : DataBuffer
-        Base buffer of this buffer
-    offset : int
-        Byte offset of this buffer relative to base buffer
-    store : bool
-        Specify whether this object stores a reference to the data,
-        allowing the data to be updated regardless of striding. Note
-        that modifying the data after passing it here might result in
-        undesired behavior, unless a copy is given. Default True.
+    data : ndarray | None
+        Buffer data.
     """
 
-    def __init__(self, data=None, dtype=None, target=gl.GL_ARRAY_BUFFER,
-                 size=0, store=True):
-        self._data = None
-        self._store = store
-        self._copied = False  # flag to indicate that a copy is made
-        self._size = size  # number of elements in buffer
-
-        # Convert data to array+dtype if needed
-        if data is not None:
-            if dtype is not None:
-                data = np.array(data, dtype=dtype, copy=False)
-            else:
-                data = np.array(data, copy=False)
-
-        # Create buffer from dtype and size
-        elif dtype is not None:
-            self._dtype = np.dtype(dtype)
-            self._size = size
-            self._stride = self._dtype.itemsize
-            self._itemsize = self._dtype.itemsize
-            self._nbytes = self._size * self._itemsize
-            if self._store:
-                self._data = np.empty(self._size, dtype=self._dtype)
-            # else:
-            #    self.set_data(data,copy=True)
-
-        # We need a minimum amount of information
-        else:
-            raise ValueError("data/dtype/base cannot be all set to None")
-        
-        Buffer.__init__(self, data=data, target=target)
-
-    @property
-    def target(self):
-        """ OpenGL type of object. """
+    def __init__(self, data=None):
+        self._size = 0  # number of elements in buffer, set in resize_bytes()
+        self._dtype = None
+        self._stride = 0
+        self._itemsize = 0
+        self._last_dim = None
+        Buffer.__init__(self, data)
+
+    def _prepare_data(self, data):
+        # Can be overrriden by subclasses
+        if not isinstance(data, np.ndarray):
+            raise TypeError("DataBuffer data must be numpy array.")
+        return data
 
-        return self._target
+    def set_subdata(self, data, offset=0, copy=False, **kwargs):
+        """ Set a sub-region of the buffer (deferred operation).
 
-    def _prepare_data(self, data, **kwds):
-        if len(kwds) > 0:
-            raise ValueError("Unexpected keyword arguments: %r" %
-                             list(kwds.keys()))
-        # Subclasses override this
-        return data
+        Parameters
+        ----------
 
-    def set_subdata(self, data, offset=0, copy=False, **kwds):
-        data = self._prepare_data(data, **kwds)
+        data : ndarray
+            Data to be uploaded
+        offset: int
+            Offset in buffer where to start copying data (in bytes)
+        copy: bool
+            Since the operation is deferred, data may change before
+            data is actually uploaded to GPU memory.
+            Asking explicitly for a copy will prevent this behavior.
+        **kwargs : dict
+            Additional keyword arguments.
+        """
+        data = self._prepare_data(data, **kwargs)
         offset = offset * self.itemsize
         Buffer.set_subdata(self, data=data, offset=offset, copy=copy)
-    
-    def set_data(self, data, copy=False, **kwds):
+
+    def set_data(self, data, copy=False, **kwargs):
         """ Set data (deferred operation)
 
         Parameters
         ----------
-
         data : ndarray
             Data to be uploaded
-        offset: int
-            Offset in buffer to start copying data (in number of vertices)
         copy: bool
             Since the operation is deferred, data may change before
             data is actually uploaded to GPU memory.
             Asking explicitly for a copy will prevent this behavior.
+        **kwargs : dict
+            Additional arguments.
         """
-        data = self._prepare_data(data, **kwds)
-        
-        # Handle storage
-        if self._store:
-            if not data.flags["C_CONTIGUOUS"]:
-                logger.warning("Copying discontiguous data as CPU storage")
-                self._copied = True
-                data = data.copy()
-            self._data = data.ravel()  # Makes a copy if not contiguous
-        # Store meta data (AFTER flattening, or stride would be wrong)
+        data = self._prepare_data(data, **kwargs)
         self._dtype = data.dtype
         self._stride = data.strides[-1]
         self._itemsize = self._dtype.itemsize
@@ -347,12 +219,6 @@ class DataBuffer(Buffer):
         return self._size
 
     @property
-    def data(self):
-        """ Buffer CPU storage """
-
-        return self._data
-
-    @property
     def itemsize(self):
         """ The total number of bytes required to store the array data """
 
@@ -362,6 +228,8 @@ class DataBuffer(Buffer):
     def glsl_type(self):
         """ GLSL declaration strings required for a variable to hold this data.
         """
+        if self.dtype is None:
+            return None
         dtshape = self.dtype[0].shape
         n = dtshape[0] if dtshape else 1
         if n > 1:
@@ -383,39 +251,23 @@ class DataBuffer(Buffer):
         This clears any pending operations.
         """
         Buffer.resize_bytes(self, size)
-
         self._size = size // self.itemsize
-        
-        if self._data is not None and self._store: 
-            if self._data.size != self._size:
-                self._data = np.resize(self._data, self._size)
-        else:
-            self._data = None
 
     def __getitem__(self, key):
         """ Create a view on this buffer. """
 
         view = DataBufferView(self, key)
-        self._views.append(view)
+        self._views.append(weakref.ref(view))
         return view
 
     def __setitem__(self, key, data):
         """ Set data (deferred operation) """
 
         # Setting a whole field of the buffer: only allowed if we have CPU
-        # storage. Note this case (key is str) only happen with base buffer
-        if isinstance(key, str):
-            if self._data is None:
-                raise ValueError(
-                    """Cannot set non contiguous """
-                    """data on buffer without CPU storage""")
-
-            # WARNING: do we check data size
-            #          or do we let numpy raises an error ?
-            self._data[key] = data
-            self.set_data(self._data, copy=False)
-            return
-
+        # storage. Note this case (key is string) only happen with base buffer
+        if isinstance(key, string_types):
+            raise ValueError("Cannot set non-contiguous data on buffer")
+        
         # Setting one or several elements
         elif isinstance(key, int):
             if key < 0:
@@ -432,34 +284,27 @@ class DataBuffer(Buffer):
         else:
             raise TypeError("Buffer indices must be integers or strings")
 
-        # Buffer is a base buffer and we have CPU storage
-        if self.data is not None:
-            # WARNING: do we check data size
-            #          or do we let numpy raises an error ?
-            self.data[key] = data
-            offset = start  # * self.itemsize
-            self.set_subdata(data=self.data[start:stop],
-                             offset=offset, copy=False)
-
-        # Buffer is a base buffer but we do not have CPU storage
-        # If 'key' points to a contiguous chunk of buffer, it's ok
-        elif step == 1:
-            offset = start  # * self.itemsize
-
-            # Make sure data is an array
-            if not isinstance(data, np.ndarray):
-                data = np.array(data, dtype=self.dtype, copy=False)
+        # Contiguous update?
+        if step != 1:
+            raise ValueError("Cannot set non-contiguous data on buffer")
 
-            # Make sure data is big enough
-            if data.size != stop - start:
-                data = np.resize(data, stop - start)
+        # Make sure data is an array
+        if not isinstance(data, np.ndarray):
+            data = np.array(data, dtype=self.dtype, copy=False)
 
-            self.set_subdata(data=data, offset=offset, copy=True)
+        # Make sure data is big enough
+        if data.size < stop - start:
+            data = np.resize(data, stop - start)
+        elif data.size > stop - start:
+            raise ValueError('Data too big to fit GPU data.')
+        
+        # Set data
+        offset = start  # * self.itemsize
+        self.set_subdata(data=data, offset=offset, copy=True)
 
-        # All the above fails, we raise an error
-        else:
-            raise ValueError(
-                "Cannot set non contiguous data on buffer without CPU storage")
+    def __repr__(self):
+        return ("<%s size=%s last_dim=%s>" % 
+                (self.__class__.__name__, self.size, self._last_dim))
 
 
 class DataBufferView(DataBuffer):
@@ -477,17 +322,22 @@ class DataBufferView(DataBuffer):
     Notes
     -----
     
-    It is gnerally not necessary to instantiate this class manually; use 
+    It is generally not necessary to instantiate this class manually; use 
     ``base_buffer[key]`` instead.
     """
-
+    
+    # Note that this class is a bit evil: it is a subclass of GLObject,
+    # Buffer and DataBuffer, but any of these __init__'s are not called ...
+    
     def __init__(self, base, key):
+        # Note how this never runs the super's __init__,
+        # all attributes must thus be set here ...
+        
         self._base = base
         self._key = key
-        self._target = base.target
         self._stride = base.stride
 
-        if isinstance(key, str):
+        if isinstance(key, string_types):
             self._dtype = base.dtype[key]
             self._offset = base.dtype.fields[key][1]
             self._nbytes = base.size * self._dtype.itemsize
@@ -518,39 +368,25 @@ class DataBufferView(DataBuffer):
         self._size = stop - start
         self._dtype = base.dtype
         self._nbytes = self.size * self.itemsize
-
+    
     @property
-    def handle(self):
-        """ Name of this object on the GPU """
-
-        return self._base.handle
-
+    def glir(self):
+        return self._base.glir
+    
     @property
-    def target(self):
-        """ OpenGL type of object. """
-
-        return self._base.target
-
-    def activate(self):
-        """ Activate the object on GPU """
-
-        self._base.activate()
-
-    def deactivate(self):
-        """ Deactivate the object on GPU """
-
-        self._base.deactivate()
-
-    def set_data(self, data, copy=False):
-        raise ValueError("Cannot set_data on buffer view; only set_subdata is "
-                         "allowed.")
+    def id(self):
+        return self._base.id
 
     @property
-    def dtype(self):
-        """ Buffer dtype """
-
-        return self._dtype
-
+    def _last_dim(self):
+        return self._base._last_dim
+    
+    def set_subdata(self, data, offset=0, copy=False, **kwargs):
+        raise RuntimeError("Cannot set data on buffer view.")
+    
+    def set_data(self, data, copy=False, **kwargs):
+        raise RuntimeError("Cannot set data on buffer view.")
+    
     @property
     def offset(self):
         """ Buffer offset (in bytes) relative to base """
@@ -558,100 +394,18 @@ class DataBufferView(DataBuffer):
         return self._offset
 
     @property
-    def stride(self):
-        """ Stride of data in memory """
-
-        return self._stride
-
-    @property
     def base(self):
         """Buffer base if this buffer is a view on another buffer. """
-
         return self._base
-
-    @property
-    def size(self):
-        """ Number of elements in the buffer """
-        return self._size
-
-    @property
-    def data(self):
-        """ Buffer CPU storage """
-
-        return self.base.data
-
-    @property
-    def itemsize(self):
-        """ The total number of bytes required to store the array data """
-
-        return self._itemsize
-
-    @property
-    def glsl_type(self):
-        """ GLSL declaration strings required for a variable to hold this data.
-        """
-        dtshape = self.dtype[0].shape
-        n = dtshape[0] if dtshape else 1
-        if n > 1:
-            dtype = 'vec%d' % n
-        else:
-            dtype = 'float' if 'f' in self.dtype[0].base.kind else 'int'
-        return 'attribute', dtype
-
+    
     def resize_bytes(self, size):
-        raise TypeError("Cannot resize buffer view.")
+        raise RuntimeError("Cannot resize buffer view.")
 
     def __getitem__(self, key):
-        """ Create a view on this buffer. """
-
-        raise ValueError("Can only access data from a base buffer")
+        raise RuntimeError("Can only access data from a base buffer")
 
     def __setitem__(self, key, data):
-        """ Set data (deferred operation) """
-
-        if not self._valid:
-            raise ValueError("This buffer view has been invalidated")
-
-        if isinstance(key, str):
-            raise ValueError(
-                "Cannot set a specific field on a non-base buffer")
-
-        elif key == Ellipsis and self.base is not None:
-            # WARNING: do we check data size
-            #          or do we let numpy raises an error ?
-            self.base[self._key] = data
-            return
-        # Setting one or several elements
-        elif isinstance(key, int):
-            if key < 0:
-                key += self.size
-            if key < 0 or key > self.size:
-                raise IndexError("Buffer assignment index out of range")
-            start, stop, step = key, key + 1, 1
-        elif isinstance(key, slice):
-            start, stop, step = key.indices(self.size)
-            if stop < start:
-                start, stop = stop, start
-        elif key == Ellipsis:
-            start, stop = 0, self.size
-        else:
-            raise TypeError("Buffer indices must be integers or strings")
-
-        # Set data on base buffer
-        base = self.base
-        # Base buffer has CPU storage
-        if base.data is not None:
-            # WARNING: do we check data size
-            #          or do we let numpy raises an error ?
-            base.data[key] = data
-            offset = start * base.itemsize
-            data = base.data[start:stop]
-            base.set_subdata(data=data, offset=offset, copy=False)
-        # Base buffer has no CPU storage, we cannot do operation
-        else:
-            raise ValueError(
-                """Cannot set non contiguous data """
-                """on buffer without CPU storage""")
+        raise RuntimeError("Cannot set data on Buffer view")
 
     def __repr__(self):
         return ("<DataBufferView on %r at offset=%d size=%d>" % 
@@ -664,77 +418,51 @@ class VertexBuffer(DataBuffer):
 
     Parameters
     ----------
-
     data : ndarray
         Buffer data (optional)
-    dtype : dtype
-        Buffer data type (optional)
-    size : int
-        Buffer size (optional)
-    store : bool
-        Specify whether this object stores a reference to the data,
-        allowing the data to be updated regardless of striding. Note
-        that modifying the data after passing it here might result in
-        undesired behavior, unless a copy is given. Default True.
     """
 
-    def __init__(self, data=None, dtype=None, size=0, store=True):
-
-        if isinstance(data, (list, tuple)):
-            data = np.array(data, np.float32)
-
-        if dtype is not None:
-            dtype = np.dtype(dtype)
-            if dtype.isbuiltin:
-                dtype = np.dtype([('f0', dtype, 1)])
-
-        DataBuffer.__init__(self, data=data, dtype=dtype, size=size,
-                            target=gl.GL_ARRAY_BUFFER,
-                            store=store)
-
-        # Check base type and count for each dtype fields (if buffer is a base)
-        for name in self.dtype.names:
-            btype = self.dtype[name].base
-            if len(self.dtype[name].shape):
-                count = 1
-                s = self.dtype[name].shape
-                for i in range(len(s)):
-                    count *= s[i]
-                #count = reduce(mul, self.dtype[name].shape)
-            else:
-                count = 1
-            if btype not in [np.int8,  np.uint8,  np.float16,
-                             np.int16, np.uint16, np.float32]:
-                msg = ("Data basetype %r not allowed for Buffer/%s" 
-                       % (btype, name))
-                raise TypeError(msg)
-            elif count not in [1, 2, 3, 4]:
-                msg = ("Data basecount %s not allowed for Buffer/%s"
-                       % (count, name))
-                raise TypeError(msg)
+    _GLIR_TYPE = 'VertexBuffer'
 
     def _prepare_data(self, data, convert=False):
         # Build a structured view of the data if:
         #  -> it is not already a structured array
         #  -> shape if 1-D or last dimension is 1,2,3 or 4
+        if isinstance(data, list):
+            data = np.array(data, dtype=np.float32)
+        if not isinstance(data, np.ndarray):
+            raise ValueError('Data must be a ndarray (got %s)' % type(data))
         if data.dtype.isbuiltin:
-            if convert is True and data.dtype is not np.float32:
+            if convert is True:
                 data = data.astype(np.float32)
-            c = data.shape[-1]
-            if data.ndim == 1 or (data.ndim == 2 and c == 1):
-                data.shape = (data.size,)  # necessary in case (N,1) array
-                data = data.view(dtype=[('f0', data.dtype.base, 1)])
-            elif c in [1, 2, 3, 4]:
+            if data.dtype in (np.float64, np.int64):
+                raise TypeError('data must be 32-bit not %s'
+                                % data.dtype)
+            c = data.shape[-1] if data.ndim > 1 else 1
+            if c in [2, 3, 4]:
                 if not data.flags['C_CONTIGUOUS']:
-                    logger.warning("Copying discontiguous data for struct "
-                                   "dtype")
+                    logger.warning('Copying discontiguous data for struct '
+                                   'dtype:\n%s' % _last_stack_str())
                     data = data.copy()
-                data = data.view(dtype=[('f0', data.dtype.base, c)])
             else:
-                data = data.view(dtype=[('f0', data.dtype.base, 1)])
+                c = 1
+            if self._last_dim and c != self._last_dim:
+                raise ValueError('Last dimension should be %s not %s'
+                                 % (self._last_dim, c))
+            data = data.view(dtype=[('f0', data.dtype.base, c)])
+            self._last_dim = c
         return data
 
 
+def _last_stack_str():
+    """Print stack trace from call that didn't originate from here"""
+    stack = extract_stack()
+    for s in stack[::-1]:
+        if op.join('vispy', 'gloo', 'buffer.py') not in __file__:
+            break
+    return format_list([s])[0]
+
+
 # ------------------------------------------------------- IndexBuffer class ---
 class IndexBuffer(DataBuffer):
     """ Buffer for index data
@@ -742,37 +470,29 @@ class IndexBuffer(DataBuffer):
     Parameters
     ----------
 
-    data : ndarray
-        Buffer data (optional)
-    dtype : dtype
-        Buffer data type (optional)
-    size : int
-        Buffer size (optional)
-    store : bool
-        Specify whether this object stores a reference to the data,
-        allowing the data to be updated regardless of striding. Note
-        that modifying the data after passing it here might result in
-        undesired behavior, unless a copy is given. Default True.
+    data : ndarray | None
+        Buffer data.
     """
+    
+    _GLIR_TYPE = 'IndexBuffer'
 
-    def __init__(self, data=None, dtype=np.uint32, size=0, store=True):
-
-        if dtype and not np.dtype(dtype).isbuiltin:
-            raise TypeError("Element buffer dtype cannot be structured")
-
-        if isinstance(data, np.ndarray):
-            pass
-        elif dtype not in [np.uint8, np.uint16, np.uint32]:
-            raise TypeError("Data type not allowed for IndexBuffer")
-
-        DataBuffer.__init__(self, data=data, dtype=dtype, size=size,
-                            target=gl.GL_ELEMENT_ARRAY_BUFFER,
-                            store=store)
+    def __init__(self, data=None):
+        DataBuffer.__init__(self, data)
+        self._last_dim = 1
 
     def _prepare_data(self, data, convert=False):
+        if isinstance(data, list):
+            data = np.array(data, dtype=np.uint32)
+        if not isinstance(data, np.ndarray):
+            raise ValueError('Data must be a ndarray (got %s)' % type(data))
         if not data.dtype.isbuiltin:
             raise TypeError("Element buffer dtype cannot be structured")
         else:
-            if convert is True and data.dtype is not np.uint32:
-                data = data.astype(np.uint32)
+            if convert:
+                if data.dtype is not np.uint32:
+                    data = data.astype(np.uint32)
+            else:
+                if data.dtype not in [np.uint32, np.uint16, np.uint8]:
+                    raise TypeError("Invalid dtype for IndexBuffer: %r" %
+                                    data.dtype)
         return data
diff --git a/vispy/gloo/context.py b/vispy/gloo/context.py
new file mode 100644
index 0000000..dedd64e
--- /dev/null
+++ b/vispy/gloo/context.py
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+Functionality to deal with GL Contexts in vispy. This module is defined
+in gloo, because gloo (and the layers that depend on it) need to be
+context aware. The vispy.app module "provides" a context, and therefore
+depends on this module. Although the GLContext class is aimed for use
+by vispy.app (for practical reasons), it should be possible to use
+GLContext without using vispy.app by overloading it in an appropriate
+manner.
+
+An GLContext object acts as a placeholder on which different parts
+of vispy (or other systems) can keep track of information related to
+an OpenGL context.
+"""
+
+from copy import deepcopy
+import weakref
+
+from .glir import GlirQueue, BaseGlirParser, GlirParser, glir_logger
+from .wrappers import BaseGlooFunctions
+from .. import config
+
+_default_dict = dict(red_size=8, green_size=8, blue_size=8, alpha_size=8,
+                     depth_size=16, stencil_size=0, double_buffer=True,
+                     stereo=False, samples=0)
+
+
+canvasses = []
+
+
+def get_default_config():
+    """Get the default OpenGL context configuration
+
+    Returns
+    -------
+    config : dict
+        Dictionary of config values.
+    """
+    return deepcopy(_default_dict)
+
+
+def get_current_canvas():
+    """ Get the currently active canvas
+    
+    Returns None if there is no canvas available. A canvas is made
+    active on initialization and before the draw event is emitted.
+    
+    When a gloo object is created, it is associated with the currently
+    active Canvas, or with the next Canvas to be created if there is
+    no current Canvas. Use Canvas.set_current() to manually activate a
+    canvas.
+    """
+    cc = [c() for c in canvasses if c() is not None]
+    if cc:
+        return cc[-1]
+    else:
+        return None
+
+
+def set_current_canvas(canvas):
+    """ Make a canvas active. Used primarily by the canvas itself.
+    """
+    # Notify glir 
+    canvas.context._do_CURRENT_command = True
+    # Try to be quick
+    if canvasses and canvasses[-1]() is canvas:
+        return
+    # Make this the current
+    cc = [c() for c in canvasses if c() is not None]
+    while canvas in cc:
+        cc.remove(canvas)
+    cc.append(canvas)
+    canvasses[:] = [weakref.ref(c) for c in cc]
+
+
+def forget_canvas(canvas):
+    """ Forget about the given canvas. Used by the canvas when closed.
+    """
+    cc = [c() for c in canvasses if c() is not None]
+    while canvas in cc:
+        cc.remove(canvas)
+    canvasses[:] = [weakref.ref(c) for c in cc]
+
+
+class GLContext(BaseGlooFunctions):
+    """An object encapsulating data necessary for a OpenGL context
+
+    Parameters
+    ----------
+    config : dict | None
+        The requested configuration.
+    shared : instance of GLContext | None
+        The shared context.
+    """
+    
+    def __init__(self, config=None, shared=None):
+        self._set_config(config)
+        self._shared = shared if (shared is not None) else GLShared()
+        assert isinstance(self._shared, GLShared)
+        self._glir = GlirQueue()
+        self._do_CURRENT_command = False  # flag that CURRENT cmd must be given
+    
+    def __repr__(self):
+        return "<GLContext at 0x%x>" % id(self)
+    
+    def _set_config(self, config):
+        self._config = deepcopy(_default_dict)
+        self._config.update(config or {})
+        # Check the config dict
+        for key, val in self._config.items():
+            if key not in _default_dict:
+                raise KeyError('Key %r is not a valid GL config key.' % key)
+            if not isinstance(val, type(_default_dict[key])):
+                raise TypeError('Context value of %r has invalid type.' % key)
+    
+    def create_shared(self, name, ref):
+        """ For the app backends to create the GLShared object.
+
+        Parameters
+        ----------
+        name : str
+            The name.
+        ref : object
+            The reference.
+        """
+        if self._shared is not None:
+            raise RuntimeError('Can only set_shared once.')
+        self._shared = GLShared(name, ref)
+    
+    @property
+    def config(self):
+        """ A dictionary describing the configuration of this GL context.
+        """
+        return self._config
+    
+    @property
+    def glir(self):
+        """ The glir queue for the context. This queue is for objects
+        that can be shared accross canvases (if they share a contex).
+        """
+        return self._glir
+    
+    @property
+    def shared(self):
+        """ Get the object that represents the namespace that can
+        potentially be shared between multiple contexts.
+        """
+        return self._shared
+    
+    def flush_commands(self, event=None):
+        """ Flush
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        if self._do_CURRENT_command:
+            self._do_CURRENT_command = False
+            self.shared.parser.parse([('CURRENT', 0)])
+        self.glir.flush(self.shared.parser)
+
+
+class GLShared(object):
+    """ Representation of a "namespace" that can be shared between
+    different contexts. App backends can associate themselves with this
+    object via add_ref().
+    
+    This object can be used to establish whether two contexts/canvases
+    share objects, and can be used as a placeholder to store shared
+    information, such as glyph atlasses.
+    """
+    
+    # We keep a (weak) ref of each backend that gets associated with
+    # this object. In theory, this means that multiple canvases can
+    # be created and also deleted; as long as there is at least one
+    # left, things should Just Work. 
+    
+    def __init__(self):
+        glir_file = config['glir_file']
+
+        parser_cls = GlirParser
+        if glir_file:
+            parser_cls = glir_logger(parser_cls, glir_file)
+
+        self._parser = parser_cls()
+        self._name = None
+        self._refs = []
+    
+    def __repr__(self):
+        return "<GLShared of %s backend at 0x%x>" % (str(self.name), id(self))
+    
+    @property
+    def parser(self):
+        """The GLIR parser (shared between contexts) """
+        return self._parser
+
+    @parser.setter
+    def parser(self, parser):
+        assert isinstance(parser, BaseGlirParser) or parser is None
+        self._parser = parser
+    
+    def add_ref(self, name, ref):
+        """ Add a reference for the backend object that gives access
+        to the low level context. Used in vispy.app.canvas.backends.
+        The given name must match with that of previously added
+        references.
+        """
+        if self._name is None:
+            self._name = name
+        elif name != self._name:
+            raise RuntimeError('Contexts can only share between backends of '
+                               'the same type')
+        self._refs.append(weakref.ref(ref))
+    
+    @property
+    def name(self):
+        """ The name of the canvas backend that this shared namespace is
+        associated with. Can be None.
+        """
+        return self._name
+    
+    @property
+    def ref(self):
+        """ A reference (stored internally via a weakref) to an object
+        that the backend system can use to obtain the low-level
+        information of the "reference context". In Vispy this will
+        typically be the CanvasBackend object.
+        """
+        # Clean
+        self._refs = [r for r in self._refs if (r() is not None)]
+        # Get ref
+        ref = self._refs[0]() if self._refs else None
+        if ref is not None:
+            return ref
+        else:
+            raise RuntimeError('No reference for available for GLShared')
+
+
+class FakeCanvas(object):
+    """ Fake canvas to allow using gloo without vispy.app
+    
+    Instantiate this class to collect GLIR commands from gloo
+    interactions. Call flush() in your draw event handler to execute
+    the commands in the active contect.
+    """
+    
+    def __init__(self):
+        self.context = GLContext()
+        set_current_canvas(self)
+    
+    def flush(self):
+        """ Flush commands. Call this after setting to context to current.
+        """
+        self.context.flush_commands()
diff --git a/vispy/gloo/framebuffer.py b/vispy/gloo/framebuffer.py
index 729a7f2..34c9f86 100644
--- a/vispy/gloo/framebuffer.py
+++ b/vispy/gloo/framebuffer.py
@@ -1,170 +1,93 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
-#import OpenGL.GL as gl
-from . import gl
 from .globject import GLObject
 from .texture import Texture2D
-from ..util import logger
+from .wrappers import _check_valid, read_pixels
+from .context import get_current_canvas
+from ..ext.six import string_types
 
 # ------------------------------------------------------ RenderBuffer class ---
 
 
 class RenderBuffer(GLObject):
     """ Base class for render buffer object
-
+    
+    A render buffer can be in color, depth or stencil format. If this
+    format is not specified, it is set when attached to the FrameBuffer.
+    
     Parameters
     ----------
-    format : GLEnum
-        The buffer format: gl.GL_RGB565, gl.GL_RGBA4, gl.GL_RGB5_A1,
-        gl.GL_DEPTH_COMPONENT16, or gl.GL_STENCIL_INDEX8
-    shape : tuple of 2 ints
-        Buffer shape (always two dimensional)
+    shape : tuple
+        The shape of the render buffer.
+    format : {None, 'color', 'depth', 'stencil'}
+        The format of the render buffer. See resize.
     resizeable : bool
         Indicates whether texture can be resized
     """
-
+    
+    _GLIR_TYPE = 'RenderBuffer'
+    
     def __init__(self, shape=None, format=None, resizeable=True):
         GLObject.__init__(self)
-        self._shape = shape
-        self._target = gl.GL_RENDERBUFFER
-        self._format = format
-        self._resizeable = resizeable
-        self._need_resize = True
-
+        self._format = None
+        self._resizeable = True
+        self.resize(shape, format)
+        self._resizeable = bool(resizeable)
+    
     @property
     def shape(self):
-        """ Buffer shape """
-
+        """RenderBuffer shape """
         return self._shape
+    
+    @property
+    def format(self):
+        """ RenderBuffer format """
 
-    def resize(self, shape):
-        """ Resize the buffer (deferred operation)
+        return self._format
+
+    def resize(self, shape, format=None):
+        """ Set the render-buffer size and format
 
         Parameters
         ----------
-
-        shape : tuple of 2 integers
-            New buffer shape (always two dimensional)
+        shape : tuple of integers
+            New shape in yx order. A render buffer is always 2D. For
+            symmetry with the texture class, a 3-element tuple can also
+            be given, in which case the last dimension is ignored.
+        format : {None, 'color', 'depth', 'stencil'}
+            The buffer format. If None, the current format is maintained. 
+            If that is also None, the format will be set upon attaching
+            it to a framebuffer. One can also specify the explicit enum:
+            GL_RGB565, GL_RGBA4, GL_RGB5_A1, GL_DEPTH_COMPONENT16, or
+            GL_STENCIL_INDEX8
         """
-
+        
         if not self._resizeable:
-            raise RuntimeError("Buffer is not resizeable")
-
-        if len(shape) != len(self.shape):
-            raise ValueError("New shape has wrong number of dimensions")
-
-        if shape == self.shape:
-            return
-
-        self._need_resize = True
-        self._shape = shape
-
-    def _create(self):
-        """ Create buffer on GPU """
-
-        logger.debug("GPU: Create render buffer")
-        self._handle = gl.glCreateRenderbuffer()
-
-    def _delete(self):
-        """ Delete buffer from GPU """
-
-        logger.debug("GPU: Deleting render buffer")
-        gl.glDeleteRenderbuffer(self._handle)
-
-    def _activate(self):
-        """ Activate buffer on GPU """
-
-        logger.debug("GPU: Activate render buffer")
-        gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._handle)
+            raise RuntimeError("RenderBuffer is not resizeable")
+        # Check shape
+        if not (isinstance(shape, tuple) and len(shape) in (2, 3)):
+            raise ValueError('RenderBuffer shape must be a 2/3 element tuple')
+        # Check format
+        if format is None:
+            format = self._format  # Use current format (may be None)
+        elif isinstance(format, int):
+            pass  # Do not check, maybe user needs desktop GL formats
+        elif isinstance(format, string_types):
+            if format not in ('color', 'depth', 'stencil'):
+                raise ValueError('RenderBuffer format must be "color", "depth"'
+                                 ' or "stencil", not %r' % format)
+        else:
+            raise ValueError('Invalid RenderBuffer format: %r' % format)
         
-        # Resize if necessary
-        if self._need_resize:
-            self._resize()
-            self._need_resize = False
-
-    def _deactivate(self):
-        """ Deactivate buffer on GPU """
-
-        logger.debug("GPU: Deactivate render buffer")
-        gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, 0)
-
-    def _resize(self):
-        """ Buffer resize on GPU """
-
-        # WARNING: Shape should be checked against maximum size
-        # maxsize = gl.glGetParameter(gl.GL_MAX_RENDERBUFFER_SIZE)
-        logger.debug("GPU: Resize render buffer")
-        gl.glRenderbufferStorage(self._target, self._format,
-                                 self._shape[1], self._shape[0])
-
-
-# ------------------------------------------------------- ColorBuffer class ---
-class ColorBuffer(RenderBuffer):
-    """ Color buffer object
-    
-    Parameters
-    ----------
-
-    format : GLEnum
-        gl.GL_RGB565, gl.GL_RGBA4, gl.GL_RGB5_A1
-    shape : tuple of 2 integers
-        Buffer shape (always two dimensional)
-    resizeable : bool
-        Indicates whether buffer can be resized
-    """
-
-    def __init__(self, shape, format=gl.GL_RGBA, resizeable=True):
-        # if format not in (gl.GL_RGB565, gl.GL_RGBA4, gl.GL_RGB5_A1):
-        #     raise ValueError("Format not allowed for color buffer")
-        RenderBuffer.__init__(self, shape, format, resizeable)
-
-
-# ------------------------------------------------------- DepthBuffer class ---
-class DepthBuffer(RenderBuffer):
-    """ Depth buffer object
-    
-    Parameters
-    ----------
-
-    shape : tuple of 2 integers
-        Buffer shape (always two dimensional)
-    format : GLEnum
-        gl.GL_DEPTH_COMPONENT16
-    resizeable : bool
-        Indicates whether buffer can be resized
-    """
-
-    def __init__(self, shape,
-                 format=gl.GL_DEPTH_COMPONENT, resizeable=True):
-        #if format not in (gl.GL_DEPTH_COMPONENT16,):
-        #    raise ValueError("Format not allowed for depth buffer")
-        RenderBuffer.__init__(self, shape, format, resizeable)
-
-
-# ----------------------------------------------------- StencilBuffer class ---
-class StencilBuffer(RenderBuffer):
-    """ Stencil buffer object
-    
-    Parameters
-    ----------
-
-    shape : tuple of 2 integers
-        Buffer shape (always two dimensional)
-    format : GLEnum
-        gl.GL_STENCIL_INDEX8
-    resizeable : bool
-        Indicates whether buffer can be resized
-    """
-
-    def __init__(self, shape,
-                 format=gl.GL_STENCIL_INDEX8, resizeable=True):
-        # if format not in (gl.GL_STENCIL_INDEX,):
-        #     raise ValueError("Format not allowed for color buffer")
-        RenderBuffer.__init__(self, shape, format, resizeable)
+        # Store and send GLIR command
+        self._shape = tuple(shape[:2])
+        self._format = format
+        if self._format is not None:
+            self._glir.command('SIZE', self._id, self._shape, self._format)
 
 
 # ------------------------------------------------------- FrameBuffer class ---
@@ -174,204 +97,158 @@ class FrameBuffer(GLObject):
     Parameters
     ----------
     
-    color : ColorBuffer (optional)
+    color : RenderBuffer (optional)
         The color buffer to attach to this frame buffer
-    depth : DepthBuffer (optional)
+    depth : RenderBuffer (optional)
         The depth buffer to attach to this frame buffer
-    stencil : StencilBuffer (optional)
+    stencil : RenderBuffer (optional)
         The stencil buffer to attach to this frame buffer
-    resizable : bool
-        Whether the buffers are resizable (default True)
     """
-
-    def __init__(self, color=None, depth=None, stencil=None, resizeable=True):
-        """
-        """
-
+    
+    _GLIR_TYPE = 'FrameBuffer'
+    
+    def __init__(self, color=None, depth=None, stencil=None):
         GLObject.__init__(self)
-
-        self._shape = None
-        self._color_buffer = color
-        self._depth_buffer = depth
-        self._stencil_buffer = stencil
-        self._need_attach = True
-        self._resizeable = resizeable
-        self._pending_attachments = []
-
+        # Init buffers
+        self._color_buffer = None
+        self._depth_buffer = None
+        self._stencil_buffer = None
         if color is not None:
             self.color_buffer = color
         if depth is not None:
             self.depth_buffer = depth
         if stencil is not None:
             self.stencil_buffer = stencil
-
+    
+    def activate(self):
+        """ Activate/use this frame buffer.
+        """
+        # Send command
+        self._glir.command('FRAMEBUFFER', self._id, True)
+        # Associate canvas now
+        canvas = get_current_canvas()
+        if canvas is not None:
+            canvas.context.glir.associate(self.glir)
+    
+    def deactivate(self):
+        """ Stop using this frame buffer, the previous framebuffer will be
+        made active.
+        """
+        self._glir.command('FRAMEBUFFER', self._id, False)
+    
     def __enter__(self):
         self.activate()
         return self
 
     def __exit__(self, t, val, trace):
         self.deactivate()
-
+    
+    def _set_buffer(self, buffer, format):
+        formats = ('color', 'depth', 'stencil')
+        assert format in formats
+        # Auto-format or check render buffer
+        if isinstance(buffer, RenderBuffer):
+            if buffer.format is None:
+                buffer.resize(buffer.shape, format)
+            elif buffer.format in formats and buffer.format != format:
+                raise ValueError('Cannot attach a %s buffer as %s buffer.' % 
+                                 (buffer.format, format)) 
+        # Attach
+        if buffer is None:
+            setattr(self, '_%s_buffer' % format, None)
+            self._glir.command('ATTACH', self._id, format, 0)
+        elif isinstance(buffer, (Texture2D, RenderBuffer)):
+            self.glir.associate(buffer.glir)
+            setattr(self, '_%s_buffer' % format, buffer)
+            self._glir.command('ATTACH', self._id, format, buffer.id)
+        else:
+            raise TypeError("Buffer must be a RenderBuffer, Texture2D or None."
+                            " (got %s)" % type(buffer))
+    
     @property
     def color_buffer(self):
         """Color buffer attachment"""
-
         return self._color_buffer
 
     @color_buffer.setter
     def color_buffer(self, buffer):
-        """Color buffer attachment"""
-
-        target = gl.GL_COLOR_ATTACHMENT0
-        if isinstance(buffer, (ColorBuffer, Texture2D)) or buffer is None:
-            self._color_buffer = buffer
-            self._pending_attachments.append((target, buffer))
-            self._need_attach = True
-        else:
-            raise TypeError("Buffer must be a ColorBuffer, Texture2D or None."
-                            " (got %s)" % type(buffer))
+        self._set_buffer(buffer, 'color')
 
     @property
     def depth_buffer(self):
         """Depth buffer attachment"""
-
         return self._depth_buffer
 
     @depth_buffer.setter
     def depth_buffer(self, buffer):
-        """Depth buffer attachment"""
-
-        target = gl.GL_DEPTH_ATTACHMENT
-        if isinstance(buffer, (DepthBuffer, Texture2D)) or buffer is None:
-            self._depth_buffer = buffer
-            self._pending_attachments.append((target, buffer))
-            self._need_attach = True
-        else:
-            raise TypeError("Buffer must be a DepthBuffer, Texture2D or None."
-                            " (got %s)" % type(buffer))
+        self._set_buffer(buffer, 'depth')
 
     @property
     def stencil_buffer(self):
         """Stencil buffer attachment"""
-
         return self._stencil_buffer
 
     @stencil_buffer.setter
     def stencil_buffer(self, buffer):
-        """Stencil buffer attachment"""
-
-        target = gl.GL_STENCIL_ATTACHMENT
-        if isinstance(buffer, StencilBuffer) or buffer is None:
-            self._stencil_buffer = buffer
-            self._pending_attachments.append((target, buffer))
-            self._need_attach = True
-        else:
-            raise TypeError("Buffer must be a StencilBuffer, Texture2D or "
-                            "None. (got %s)" % type(buffer))
+        self._set_buffer(buffer, 'stencil')
 
     @property
     def shape(self):
-        """ Buffer shape """
-
-        return self._shape
-
-    def resize(self, shape):
-        """ Resize the buffer (deferred operation)
-
-        Parameters
-        ----------
-
-        shape : tuple of 2 integers
-            New buffer shape (always two dimensional)
+        """ The shape of the Texture/RenderBuffer attached to this FrameBuffer
         """
-
-        if not self._resizeable:
-            raise RuntimeError("FrameBuffer is not resizeable")
-
-        if len(shape) != 2:
-            raise ValueError("New shape has wrong number of dimensions")
-
         if self.color_buffer is not None:
-            self.color_buffer.resize(shape)
+            return self.color_buffer.shape[:2]  # in case its a texture
         if self.depth_buffer is not None:
-            self.depth_buffer.resize(shape)
+            return self.depth_buffer.shape[:2]
         if self.stencil_buffer is not None:
-            self.stencil_buffer.resize(shape)
-
-    def _create(self):
-        """ Create framebuffer on GPU """
-
-        logger.debug("GPU: Create framebuffer")
-        self._handle = gl.glCreateFramebuffer()
-
-    def _delete(self):
-        """ Delete buffer from GPU """
-
-        logger.debug("GPU: Delete framebuffer")
-        gl.glDeleteFramebuffer(self._handle)
-
-    def _activate(self):
-        """ Activate framebuffer on GPU """
-
-        logger.debug("GPU: Activate render framebuffer")
-        gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._handle)
-        
-        # Attach buffers if necessary
-        if self._need_attach:
-            self._attach()
-            self._need_attach = False
+            return self.stencil_buffer.shape[:2]
+        raise RuntimeError('FrameBuffer without buffers has undefined shape')
     
-    def _deactivate(self):
-        """ Deactivate framebuffer on GPU """
-
-        logger.debug("GPU: Deactivate render framebuffer")
-        gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
+    def resize(self, shape):
+        """ Resize all attached buffers with the given shape
 
-    def _attach(self):
-        """ Attach render buffers to framebuffer """
+        Parameters
+        ----------
+        shape : tuple of two integers
+            New buffer shape (h, w), to be applied to all currently
+            attached buffers. For buffers that are a texture, the number
+            of color channels is preserved.
+        """
+        # Check
+        if not (isinstance(shape, tuple) and len(shape) == 2):
+            raise ValueError('RenderBuffer shape must be a 2-element tuple')
+        # Resize our buffers
+        for buf in (self.color_buffer, self.depth_buffer, self.stencil_buffer):
+            if buf is None:
+                continue
+            shape_ = shape
+            if isinstance(buf, Texture2D):
+                shape_ = shape + (self.color_buffer.shape[-1], )
+            buf.resize(shape_, buf.format)
+    
+    def read(self, mode='color', alpha=True):
+        """ Return array of pixel values in an attached buffer
         
-        # todo: this can currently only attach to texture mipmap level 0
+        Parameters
+        ----------
+        mode : str
+            The buffer type to read. May be 'color', 'depth', or 'stencil'.
+        alpha : bool
+            If True, returns RGBA array. Otherwise, returns RGB.
         
-        logger.debug("GPU: Attach render buffers")
-        while self._pending_attachments:
-            attachment, buffer = self._pending_attachments.pop(0)
-            if buffer is None:
-                gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, attachment,
-                                             gl.GL_RENDERBUFFER, 0)
-            elif isinstance(buffer, RenderBuffer):
-                buffer.activate()
-                gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, attachment,
-                                             gl.GL_RENDERBUFFER, buffer.handle)
-                buffer.deactivate()
-            elif isinstance(buffer, Texture2D):
-                buffer.activate()
-                # INFO: 0 is for mipmap level 0 (default) of the texture
-                gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, attachment,
-                                          buffer.target, buffer.handle, 0)
-                buffer.deactivate()
-            else:
-                raise ValueError("Invalid attachment: %s" % type(buffer))
-
-        if 1:
-            res = gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER)
-            if res == gl.GL_FRAMEBUFFER_COMPLETE:
-                pass
-            elif res == 0:
-                raise RuntimeError('Target not equal to GL_FRAMEBUFFER')
-            elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
-                raise RuntimeError(
-                    'FrameBuffer attachments are incomplete.')
-            elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
-                raise RuntimeError(
-                    'No valid attachments in the FrameBuffer.')
-            elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
-                raise RuntimeError(
-                    'attachments do not have the same width and height.')
-            #elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_FORMATS: # not in es 2.0
-            #    raise RuntimeError('Internal format of attachment '
-            #                       'is not renderable.')
-            elif res == gl.GL_FRAMEBUFFER_UNSUPPORTED:
-                raise RuntimeError('Combination of internal formats used '
-                                   'by attachments is not supported.')
-            else:
-                raise RuntimeError('Unknown framebuffer error: %r.' % res)
+        Returns
+        -------
+        buffer : array
+            3D array of pixels in np.uint8 format. 
+            The array shape is (h, w, 3) or (h, w, 4), with the top-left 
+            corner of the framebuffer at index [0, 0] in the returned array.
+        
+        """
+        _check_valid('mode', mode, ['color', 'depth', 'stencil'])
+        buffer = getattr(self, mode+'_buffer')
+        h, w = buffer.shape[:2]
+        
+        # todo: this is ostensibly required, but not available in gloo.gl
+        #gl.glReadBuffer(buffer._target)
+        
+        return read_pixels((0, 0, w, h), alpha=alpha)
diff --git a/vispy/gloo/gl/__init__.py b/vispy/gloo/gl/__init__.py
index e57b22a..93813fa 100644
--- a/vispy/gloo/gl/__init__.py
+++ b/vispy/gloo/gl/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ This module provides a (functional) API to OpenGL ES 2.0.
@@ -97,7 +97,7 @@ proxy = MainProxy()
 _debug_proxy = DebugProxy()
 
 
-def use_gl(target='desktop'):
+def use_gl(target='gl2'):
     """ Let Vispy use the target OpenGL ES 2.0 implementation
     
     Also see ``vispy.use()``.
@@ -108,18 +108,24 @@ def use_gl(target='desktop'):
         The target GL backend to use.
 
     Available backends:
-    * desktop - Use desktop (i.e. normal) OpenGL.
-    * pyopengl - Use pyopengl (for fallback and testing). 
-    * angle - Use the Angle library to target DirectX (Windows only). (WIP)
-    * mock - Dummy backend that can be useful for testing. (not yet available)
-    * webgl - Send the GL commands to the browser. (not yet available)
-
+    * gl2 - Use ES 2.0 subset of desktop (i.e. normal) OpenGL
+    * gl+ - Use the desktop ES 2.0 subset plus all non-deprecated GL
+      functions on your system (requires PyOpenGL)
+    * es2 - Use the ES2 library (Angle/DirectX on Windows)
+    * pyopengl2 - Use ES 2.0 subset of pyopengl (for fallback and testing)
+    * dummy - Prevent usage of gloo.gl (for when rendering occurs elsewhere)
+    
+    You can use vispy's config option "gl_debug" to check for errors
+    on each API call. Or, one can specify it as the target, e.g. "gl2
+    debug". (Debug does not apply to 'gl+', since PyOpenGL has its own
+    debug mechanism)
     """
-    target = target or 'desktop'
-
+    target = target or 'gl2'
+    target = target.replace('+', 'plus')
+    
     # Get options
     target, _, options = target.partition(' ')
-    debug = config['gl_debug'] or ('debug' in options)
+    debug = config['gl_debug'] or 'debug' in options
     
     # Select modules to import names from
     try:
@@ -131,13 +137,30 @@ def use_gl(target='desktop'):
     # Apply
     global current_backend
     current_backend = mod
-    if debug:
+    _clear_namespace()
+    if 'plus' in target:
+        # Copy PyOpenGL funcs, extra funcs, constants, no debug
+        _copy_gl_functions(mod._pyopengl2, globals())
+        _copy_gl_functions(mod, globals(), True)
+    elif debug:
         _copy_gl_functions(_debug_proxy, globals())
     else:
         _copy_gl_functions(mod, globals())
 
 
-def _copy_gl_functions(source, dest):
+def _clear_namespace():
+    """ Clear names that are not part of the strict ES API
+    """
+    ok_names = set(default_backend.__dict__)
+    ok_names.update(['gl2', 'glplus'])  # don't remove the module
+    NS = globals()
+    for name in list(NS.keys()):
+        if name.lower().startswith('gl'):
+            if name not in ok_names:
+                del NS[name]
+
+
+def _copy_gl_functions(source, dest, constants=False):
     """ Inject all objects that start with 'gl' from the source
     into the dest. source and dest can be dicts, modules or BaseGLProxy's.
     """
@@ -151,10 +174,15 @@ def _copy_gl_functions(source, dest):
         source = source.__dict__
     if not isinstance(dest, dict):
         dest = dest.__dict__
-    # Make selection of names
+    # Copy names
     funcnames = [name for name in source.keys() if name.startswith('gl')]
     for name in funcnames:
         dest[name] = source[name]
+    # Copy constants
+    if constants:
+        constnames = [name for name in source.keys() if name.startswith('GL_')]
+        for name in constnames:
+            dest[name] = source[name]
 
 
 def check_error(when='periodic check'):
@@ -182,7 +210,7 @@ def check_error(when='periodic check'):
 
 
 # Load default gl backend
-from . import desktop as default_backend  # noqa
+from . import gl2 as default_backend  # noqa
 
 # Call use to start using our default backend
 use_gl()
diff --git a/vispy/gloo/gl/_angle.py b/vispy/gloo/gl/_es2.py
similarity index 99%
rename from vispy/gloo/gl/_angle.py
rename to vispy/gloo/gl/_es2.py
index 5e18868..85b7225 100644
--- a/vispy/gloo/gl/_angle.py
+++ b/vispy/gloo/gl/_es2.py
@@ -2,12 +2,12 @@
 
 THIS CODE IS AUTO-GENERATED. DO NOT EDIT.
 
-GL ES 2.0 API based on the Angle library (i.e. DirectX)
+GL ES 2.0 API (via Angle/DirectX on Windows)
 
 """
 
 import ctypes
-from .angle import _lib
+from .es2 import _lib
 
 
 _lib.glActiveTexture.argtypes = ctypes.c_uint,
@@ -586,7 +586,7 @@ def glGetParameter(pname):
         return _glGetIntegerv(pname)
     name = pname
     res = _lib.glGetString(name)
-    return res.decode('utf-8') if res else ''
+    return ctypes.string_at(res).decode('utf-8') if res else ''
 
 
 _lib.glGetTexParameterfv.argtypes = ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_float),
@@ -727,8 +727,9 @@ _lib.glReadPixels.argtypes = ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_
 def glReadPixels(x, y, width, height, format, type):
     # GL_ALPHA, GL_RGB, GL_RGBA
     t = {6406:1, 6407:3, 6408:4}[format]
-    # we kind of only support type GL_UNSIGNED_BYTE
-    size = int(width*height*t)
+    # GL_UNSIGNED_BYTE, GL_FLOAT
+    nb = {5121:1, 5126:4}[type]
+    size = int(width*height*t*nb)
     pixels = ctypes.create_string_buffer(size)
     res = _lib.glReadPixels(x, y, width, height, format, type, pixels)
     return pixels[:]
diff --git a/vispy/gloo/gl/_desktop.py b/vispy/gloo/gl/_gl2.py
similarity index 99%
rename from vispy/gloo/gl/_desktop.py
rename to vispy/gloo/gl/_gl2.py
index e531c66..9783e24 100644
--- a/vispy/gloo/gl/_desktop.py
+++ b/vispy/gloo/gl/_gl2.py
@@ -7,7 +7,7 @@ Subset of desktop GL API compatible with GL ES 2.0
 """
 
 import ctypes
-from .desktop import _lib, _get_gl_func
+from .gl2 import _lib, _get_gl_func
 
 
 # void = glActiveTexture(GLenum texture)
@@ -796,7 +796,7 @@ def glGetParameter(pname):
     except AttributeError:
         nativefunc = glGetParameter._native = _get_gl_func("glGetString", ctypes.c_char_p, (ctypes.c_uint,))
     res = nativefunc(name)
-    return res.decode('utf-8') if res else ''
+    return ctypes.string_at(res).decode('utf-8') if res else ''
 
 
 # void = glGetTexParameterfv(GLenum target, GLenum pname, GLfloat* params)
@@ -979,8 +979,9 @@ def glPolygonOffset(factor, units):
 def glReadPixels(x, y, width, height, format, type):
     # GL_ALPHA, GL_RGB, GL_RGBA
     t = {6406:1, 6407:3, 6408:4}[format]
-    # we kind of only support type GL_UNSIGNED_BYTE
-    size = int(width*height*t)
+    # GL_UNSIGNED_BYTE, GL_FLOAT
+    nb = {5121:1, 5126:4}[type]
+    size = int(width*height*t*nb)
     pixels = ctypes.create_string_buffer(size)
     try:
         nativefunc = glReadPixels._native
diff --git a/vispy/gloo/gl/_proxy.py b/vispy/gloo/gl/_proxy.py
index 9a89ea4..3702b91 100644
--- a/vispy/gloo/gl/_proxy.py
+++ b/vispy/gloo/gl/_proxy.py
@@ -13,10 +13,6 @@ class BaseGLProxy(object):
    
     def __call__(self, funcname, returns, *args):
         raise NotImplementedError()
-    
-    
-    def glShaderSource_compat(self, handle, code):
-        return self("glShaderSource_compat", True, handle, code)
 
 
     def glActiveTexture(self, texture):
diff --git a/vispy/gloo/gl/_pyopengl.py b/vispy/gloo/gl/_pyopengl2.py
similarity index 91%
rename from vispy/gloo/gl/_pyopengl.py
rename to vispy/gloo/gl/_pyopengl2.py
index d1f5133..76d253b 100644
--- a/vispy/gloo/gl/_pyopengl.py
+++ b/vispy/gloo/gl/_pyopengl2.py
@@ -117,7 +117,7 @@ def glGetFramebufferAttachmentParameter(target, attachment, pname):
 
 def glGetProgramInfoLog(program):
     res = GL.glGetProgramInfoLog(program)
-    return res.decode('utf-8')
+    return res.decode('utf-8') if isinstance(res, bytes) else res
 
 
 def glGetRenderbufferParameter(target, pname):
@@ -129,7 +129,7 @@ def glGetRenderbufferParameter(target, pname):
 
 def glGetShaderInfoLog(shader):
     res = GL.glGetShaderInfoLog(shader)
-    return res.decode('utf-8')
+    return res.decode('utf-8') if isinstance(res, bytes) else res
 
 
 def glGetShaderSource(shader):
@@ -333,3 +333,38 @@ _functions_to_import = [
     ("glVertexAttrib4f", "glVertexAttrib4f"),
     ("glViewport", "glViewport"),
     ]
+
+# List of functions in OpenGL.GL that we use
+_used_functions = [
+    "glBindAttribLocation",
+    "glBufferData",
+    "glBufferSubData",
+    "glCompressedTexImage2D",
+    "glCompressedTexSubImage2D",
+    "glDeleteBuffers",
+    "glDeleteFramebuffers",
+    "glDeleteRenderbuffers",
+    "glDeleteTextures",
+    "glDrawElements",
+    "glGenBuffers",
+    "glGenFramebuffers",
+    "glGenRenderbuffers",
+    "glGenTextures",
+    "glGetActiveAttrib",
+    "glGetActiveUniform",
+    "glGetAttribLocation",
+    "glGetFramebufferAttachmentParameteriv",
+    "glGetProgramInfoLog",
+    "glGetRenderbufferParameteriv",
+    "glGetShaderInfoLog",
+    "glGetShaderSource",
+    "glGetString",
+    "glGetUniformfv",
+    "glGetUniformLocation",
+    "glGetVertexAttribfv",
+    "glGetVertexAttribPointerv",
+    "glShaderSource",
+    "glTexImage2D",
+    "glTexSubImage2D",
+    "glVertexAttribPointer",
+    ]
diff --git a/vispy/gloo/gl/angle.py b/vispy/gloo/gl/angle.py
deleted file mode 100644
index e1e8f28..0000000
--- a/vispy/gloo/gl/angle.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-""" GL ES 2.0 API implemented via Angle (i.e translated to DirectX).
-"""
-
-import ctypes
-
-from . import _copy_gl_functions
-from ._constants import *  # noqa
-
-
-## Ctypes stuff
-
-# todo: were are we going to put our libs?
-dirname = r'C:\Users\Almar\AppData\Local\Chromium\Application\34.0.1790.0'
-
-# Load dependency (so that libGLESv2 can find it
-fname = dirname + r'\d3dcompiler_46.dll'
-_libdum = ctypes.windll.LoadLibrary(fname)
-
-# Load GL ES 2.0 lib (Angle)
-fname = dirname + r'\libGLESv2.dll'
-_lib = ctypes.windll.LoadLibrary(fname)
-
-
-## Compatibility
-
-
-def glShaderSource_compat(handle, code):
-    """ Easy for real ES 2.0.
-    """
-    glShaderSource(handle, [code])
-    return []
-
-
-## Inject
-
-
-from . import _angle
-_copy_gl_functions(_angle, globals())
diff --git a/vispy/gloo/gl/dummy.py b/vispy/gloo/gl/dummy.py
new file mode 100644
index 0000000..01dd31d
--- /dev/null
+++ b/vispy/gloo/gl/dummy.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+""" A dummy backend.
+"""
+
+from . import BaseGLProxy, _copy_gl_functions
+from ._constants import *  # noqa
+
+
+class DummyProxy(BaseGLProxy):
+    """ A dummy backend that can be activated when the GL is not
+    processed in this process. Each GL function call will raise an
+    error.
+    """
+    
+    def __call__(self, funcname, returns, *args):
+        raise RuntimeError('Cannot call %r (or any other GL function), '
+                           'since GL is disabled.' % funcname)
+
+
+# Instantiate proxy and inject functions
+_proxy = DummyProxy()
+_copy_gl_functions(_proxy, globals())
diff --git a/vispy/gloo/gl/es2.py b/vispy/gloo/gl/es2.py
new file mode 100644
index 0000000..5262fcf
--- /dev/null
+++ b/vispy/gloo/gl/es2.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+""" GL ES 2.0 API. 
+On Windows implemented via Angle (i.e translated to DirectX).
+"""
+
+import sys
+import os
+import ctypes
+
+from . import _copy_gl_functions
+from ._constants import *  # noqa
+
+
+## Ctypes stuff
+
+if hasattr(ctypes, 'TEST_DLL'):
+    # Load dummy lib 
+    _lib = ctypes.TEST_DLL.LoadLibrary('')
+    
+elif sys.platform.startswith('win'):
+    raise RuntimeError('ES 2.0 is not available on Windows yet')
+    
+    # todo: were are we going to put our libs?
+    dirname = r'C:\Users\Almar\AppData\Local\Chromium\Application\34.0.1790.0'
+    
+    # Load dependency (so that libGLESv2 can find it
+    fname = dirname + r'\d3dcompiler_46.dll'
+    _libdum = ctypes.windll.LoadLibrary(fname)
+    
+    # Load GL ES 2.0 lib (Angle)
+    fname = dirname + r'\libGLESv2.dll'
+    _lib = ctypes.windll.LoadLibrary(fname)
+
+elif sys.platform.startswith('linux'):
+    es2_file = None
+    # Load from env
+    if 'ES2_LIBRARY' in os.environ:  # todo: is this the correct name?
+        if os.path.exists(os.environ['ES2_LIBRARY']):
+            es2_file = os.path.realpath(os.environ['ES2_LIBRARY'])
+    # Else, try to find it
+    if es2_file is None:
+        es2_file = ctypes.util.find_library('GLESv2')
+    # Else, we failed and exit
+    if es2_file is None:
+        raise OSError('GL ES 2.0 library not found')
+    # Load it
+    _lib = ctypes.CDLL(es2_file)
+    
+elif sys.platform.startswith('darwin'):
+    raise RuntimeError('ES 2.0 is not available on OSX yet')
+
+else:
+    raise RuntimeError('Unknown platform: %s' % sys.platform)
+
+
+## Inject
+
+from . import _es2  # noqa
+_copy_gl_functions(_es2, globals())
diff --git a/vispy/gloo/gl/desktop.py b/vispy/gloo/gl/gl2.py
similarity index 56%
rename from vispy/gloo/gl/desktop.py
rename to vispy/gloo/gl/gl2.py
index c50d6b4..91dbc27 100644
--- a/vispy/gloo/gl/desktop.py
+++ b/vispy/gloo/gl/gl2.py
@@ -1,23 +1,31 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ GL ES 2.0 API implemented via desktop GL (i.e subset of normal OpenGL).
 """
 
+import os
 import sys
 import ctypes.util
 
 from . import _copy_gl_functions
 from ._constants import *  # noqa
 
-## Ctypes stuff
+# Ctypes stuff
 
 
 # Load the OpenGL library. We more or less follow the same approach
 # as PyOpenGL does internally
 
-if sys.platform.startswith('win'):
+_have_get_proc_address = False
+_lib = os.getenv('VISPY_GL_LIB', '')
+if _lib != '':
+    if sys.platform.startswith('win'):
+        _lib = ctypes.windll.LoadLibrary(_lib)
+    else:
+        _lib = ctypes.cdll.LoadLibrary(_lib)
+elif sys.platform.startswith('win'):
     # Windows
     _lib = ctypes.windll.opengl32
     try:
@@ -27,11 +35,9 @@ if sys.platform.startswith('win'):
         wglGetProcAddress.argtypes = [ctypes.c_char_p]
         _have_get_proc_address = True
     except AttributeError:
-        _have_get_proc_address = False
+        pass
 else:
     # Unix-ish
-    _have_get_proc_address = False
-    # Get filename
     if sys.platform.startswith('darwin'):
         _fname = ctypes.util.find_library('OpenGL')
     else:
@@ -71,51 +77,7 @@ def _get_gl_func(name, restype, argtypes):
         raise RuntimeError('Function %s not present in context.' % name)
 
 
-## Compatibility
-
-
-def glShaderSource_compat(handle, code):
-    """ This version of glShaderSource applies small modifications
-    to the given GLSL code in order to make it more compatible between
-    desktop and ES2.0 implementations. Specifically:
-      * It sets the #version pragma (if none is given already)
-      * It returns a (possibly empty) set of enums that should be enabled
-        (for automatically enabling point sprites)
-    """
-
-    # Make a string
-    if isinstance(code, (list, tuple)):
-        code = '\n'.join(code)
-
-    # Determine whether this is a vertex or fragment shader
-    code_ = '\n' + code
-    is_vertex = '\nattribute' in code_
-    is_fragment = not is_vertex
-
-    # Determine whether to write the #version pragma
-    write_version = True
-    for line in code.splitlines():
-        if line.startswith('#version'):
-            write_version = False
-            logger.warning('For compatibility accross different GL backends, '
-                           'avoid using the #version pragma.')
-    if write_version:
-        code = '#version 120\n#line 0\n' + code
-
-    # Do the call
-    glShaderSource(handle, [code])
-
-    # Determine whether to activate point sprites
-    enums = set()
-    if is_fragment and 'gl_PointCoord' in code:
-        enums.add(Enum('GL_VERTEX_PROGRAM_POINT_SIZE', 34370))
-        enums.add(Enum('GL_POINT_SPRITE', 34913))
-    return enums
-    return []
-
-
-## Inject
-
+# Inject
 
-from . import _desktop
-_copy_gl_functions(_desktop, globals())
+from . import _gl2  # noqa
+_copy_gl_functions(_gl2, globals())
diff --git a/vispy/gloo/gl/glplus.py b/vispy/gloo/gl/glplus.py
new file mode 100644
index 0000000..72cbd0d
--- /dev/null
+++ b/vispy/gloo/gl/glplus.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+""" This module provides a namespace for additional desktop OpenGL functions.
+
+The functions in this module are copied from PyOpenGL, but any deprecated
+functions are omitted, as well as any functions that are in our ES 2.0 API.
+
+"""
+
+from OpenGL import GL as _GL
+from . import _pyopengl2
+from . import _constants
+
+
+def _inject():
+    """ Inject functions and constants from PyOpenGL but leave out the
+    names that are deprecated or that we provide in our API.
+    """
+    
+    # Get namespaces
+    NS = globals()
+    GLNS = _GL.__dict__
+    
+    # Get names that we use in our API
+    used_names = []
+    used_names.extend([names[0] for names in _pyopengl2._functions_to_import])
+    used_names.extend([name for name in _pyopengl2._used_functions])
+    NS['_used_names'] = used_names
+    #
+    used_constants = set(_constants.__dict__)
+    # Count
+    injected_constants = 0
+    injected_functions = 0
+    
+    for name in dir(_GL):
+        
+        if name.startswith('GL_'):
+            # todo: find list of deprecated constants
+            if name not in used_constants:
+                NS[name] = GLNS[name]
+                injected_constants += 1
+        
+        elif name.startswith('gl'):
+            # Functions
+            if (name + ',') in _deprecated_functions:
+                pass  # Function is deprecated
+            elif name in used_names:
+                pass  # Function is in our GL ES 2.0 API
+            else:
+                NS[name] = GLNS[name]
+                injected_functions += 1
+    
+    #print('injected %i constants and %i functions in glplus' % 
+    #      (injected_constants, injected_functions))
+
+
+# List of deprecated functions, obtained by parsing gl.spec
+_deprecated_functions = """
+    glAccum, glAlphaFunc, glAreTexturesResident, glArrayElement, glBegin, 
+    glBitmap, glCallList, glCallLists, glClearAccum, glClearIndex, 
+    glClientActiveTexture, glClipPlane, glColor3b, glColor3bv, glColor3d, 
+    glColor3dv, glColor3f, glColor3fv, glColor3i, glColor3iv, glColor3s, 
+    glColor3sv, glColor3ub, glColor3ubv, glColor3ui, glColor3uiv, glColor3us, 
+    glColor3usv, glColor4b, glColor4bv, glColor4d, glColor4dv, glColor4f, 
+    glColor4fv, glColor4i, glColor4iv, glColor4s, glColor4sv, glColor4ub, 
+    glColor4ubv, glColor4ui, glColor4uiv, glColor4us, glColor4usv, 
+    glColorMaterial, glColorPointer, glColorSubTable, glColorTable, 
+    glColorTableParameterfv, glColorTableParameteriv, glConvolutionFilter1D, 
+    glConvolutionFilter2D, glConvolutionParameterf, glConvolutionParameterfv, 
+    glConvolutionParameteri, glConvolutionParameteriv, glCopyColorSubTable, 
+    glCopyColorTable, glCopyConvolutionFilter1D, glCopyConvolutionFilter2D, 
+    glCopyPixels, glDeleteLists, glDisableClientState, glDrawPixels, 
+    glEdgeFlag, glEdgeFlagPointer, glEdgeFlagv, glEnableClientState, glEnd, 
+    glEndList, glEvalCoord1d, glEvalCoord1dv, glEvalCoord1f, glEvalCoord1fv, 
+    glEvalCoord2d, glEvalCoord2dv, glEvalCoord2f, glEvalCoord2fv, 
+    glEvalMesh1, glEvalMesh2, glEvalPoint1, glEvalPoint2, glFeedbackBuffer, 
+    glFogCoordPointer, glFogCoordd, glFogCoorddv, glFogCoordf, glFogCoordfv, 
+    glFogf, glFogfv, glFogi, glFogiv, glFrustum, glGenLists, glGetClipPlane, 
+    glGetColorTable, glGetColorTableParameterfv, glGetColorTableParameteriv, 
+    glGetConvolutionFilter, glGetConvolutionParameterfv, 
+    glGetConvolutionParameteriv, glGetHistogram, glGetHistogramParameterfv, 
+    glGetHistogramParameteriv, glGetLightfv, glGetLightiv, glGetMapdv, 
+    glGetMapfv, glGetMapiv, glGetMaterialfv, glGetMaterialiv, glGetMinmax, 
+    glGetMinmaxParameterfv, glGetMinmaxParameteriv, glGetPixelMapfv, 
+    glGetPixelMapuiv, glGetPixelMapusv, glGetPolygonStipple, 
+    glGetSeparableFilter, glGetTexEnvfv, glGetTexEnviv, glGetTexGendv, 
+    glGetTexGenfv, glGetTexGeniv, glHistogram, glIndexMask, glIndexPointer, 
+    glIndexd, glIndexdv, glIndexf, glIndexfv, glIndexi, glIndexiv, glIndexs, 
+    glIndexsv, glInitNames, glInterleavedArrays, glIsList, glLightModelf, 
+    glLightModelfv, glLightModeli, glLightModeliv, glLightf, glLightfv, 
+    glLighti, glLightiv, glLineStipple, glListBase, glLoadIdentity, 
+    glLoadMatrixd, glLoadMatrixf, glLoadName, glLoadTransposeMatrixd, 
+    glLoadTransposeMatrixf, glMap1d, glMap1f, glMap2d, glMap2f, glMapGrid1d, 
+    glMapGrid1f, glMapGrid2d, glMapGrid2f, glMaterialf, glMaterialfv, 
+    glMateriali, glMaterialiv, glMatrixMode, glMinmax, glMultMatrixd, 
+    glMultMatrixf, glMultTransposeMatrixd, glMultTransposeMatrixf, 
+    glMultiTexCoord1d, glMultiTexCoord1dv, glMultiTexCoord1f, 
+    glMultiTexCoord1fv, glMultiTexCoord1i, glMultiTexCoord1iv, 
+    glMultiTexCoord1s, glMultiTexCoord1sv, glMultiTexCoord2d, 
+    glMultiTexCoord2dv, glMultiTexCoord2f, glMultiTexCoord2fv, 
+    glMultiTexCoord2i, glMultiTexCoord2iv, glMultiTexCoord2s, 
+    glMultiTexCoord2sv, glMultiTexCoord3d, glMultiTexCoord3dv, 
+    glMultiTexCoord3f, glMultiTexCoord3fv, glMultiTexCoord3i, 
+    glMultiTexCoord3iv, glMultiTexCoord3s, glMultiTexCoord3sv, 
+    glMultiTexCoord4d, glMultiTexCoord4dv, glMultiTexCoord4f, 
+    glMultiTexCoord4fv, glMultiTexCoord4i, glMultiTexCoord4iv, 
+    glMultiTexCoord4s, glMultiTexCoord4sv, glNewList, glNormal3b, 
+    glNormal3bv, glNormal3d, glNormal3dv, glNormal3f, glNormal3fv, 
+    glNormal3i, glNormal3iv, glNormal3s, glNormal3sv, glNormalPointer, 
+    glOrtho, glPassThrough, glPixelMapfv, glPixelMapuiv, glPixelMapusv, 
+    glPixelTransferf, glPixelTransferi, glPixelZoom, glPolygonStipple, 
+    glPopAttrib, glPopClientAttrib, glPopMatrix, glPopName, 
+    glPrioritizeTextures, glPushAttrib, glPushClientAttrib, glPushMatrix, 
+    glPushName, glRasterPos2d, glRasterPos2dv, glRasterPos2f, glRasterPos2fv, 
+    glRasterPos2i, glRasterPos2iv, glRasterPos2s, glRasterPos2sv, 
+    glRasterPos3d, glRasterPos3dv, glRasterPos3f, glRasterPos3fv, 
+    glRasterPos3i, glRasterPos3iv, glRasterPos3s, glRasterPos3sv, 
+    glRasterPos4d, glRasterPos4dv, glRasterPos4f, glRasterPos4fv, 
+    glRasterPos4i, glRasterPos4iv, glRasterPos4s, glRasterPos4sv, glRectd, 
+    glRectdv, glRectf, glRectfv, glRecti, glRectiv, glRects, glRectsv, 
+    glRenderMode, glResetHistogram, glResetMinmax, glRotated, glRotatef, 
+    glScaled, glScalef, glSecondaryColor3b, glSecondaryColor3bv, 
+    glSecondaryColor3d, glSecondaryColor3dv, glSecondaryColor3f, 
+    glSecondaryColor3fv, glSecondaryColor3i, glSecondaryColor3iv, 
+    glSecondaryColor3s, glSecondaryColor3sv, glSecondaryColor3ub, 
+    glSecondaryColor3ubv, glSecondaryColor3ui, glSecondaryColor3uiv, 
+    glSecondaryColor3us, glSecondaryColor3usv, glSecondaryColorPointer, 
+    glSelectBuffer, glSeparableFilter2D, glShadeModel, glTexCoord1d, 
+    glTexCoord1dv, glTexCoord1f, glTexCoord1fv, glTexCoord1i, glTexCoord1iv, 
+    glTexCoord1s, glTexCoord1sv, glTexCoord2d, glTexCoord2dv, glTexCoord2f, 
+    glTexCoord2fv, glTexCoord2i, glTexCoord2iv, glTexCoord2s, glTexCoord2sv, 
+    glTexCoord3d, glTexCoord3dv, glTexCoord3f, glTexCoord3fv, glTexCoord3i, 
+    glTexCoord3iv, glTexCoord3s, glTexCoord3sv, glTexCoord4d, glTexCoord4dv, 
+    glTexCoord4f, glTexCoord4fv, glTexCoord4i, glTexCoord4iv, glTexCoord4s, 
+    glTexCoord4sv, glTexCoordPointer, glTexEnvf, glTexEnvfv, glTexEnvi, 
+    glTexEnviv, glTexGend, glTexGendv, glTexGenf, glTexGenfv, glTexGeni, 
+    glTexGeniv, glTranslated, glTranslatef, glVertex2d, glVertex2dv, 
+    glVertex2f, glVertex2fv, glVertex2i, glVertex2iv, glVertex2s, 
+    glVertex2sv, glVertex3d, glVertex3dv, glVertex3f, glVertex3fv, 
+    glVertex3i, glVertex3iv, glVertex3s, glVertex3sv, glVertex4d, 
+    glVertex4dv, glVertex4f, glVertex4fv, glVertex4i, glVertex4iv, 
+    glVertex4s, glVertex4sv, glVertexAttrib1d, glVertexAttrib1dv, 
+    glVertexAttrib1f, glVertexAttrib1fv, glVertexAttrib1s, glVertexAttrib1sv, 
+    glVertexAttrib2d, glVertexAttrib2dv, glVertexAttrib2f, glVertexAttrib2fv, 
+    glVertexAttrib2s, glVertexAttrib2sv, glVertexAttrib3d, glVertexAttrib3dv, 
+    glVertexAttrib3f, glVertexAttrib3fv, glVertexAttrib3s, glVertexAttrib3sv, 
+    glVertexAttrib4Nbv, glVertexAttrib4Niv, glVertexAttrib4Nsv, 
+    glVertexAttrib4Nub, glVertexAttrib4Nubv, glVertexAttrib4Nuiv, 
+    glVertexAttrib4Nusv, glVertexAttrib4bv, glVertexAttrib4d, 
+    glVertexAttrib4dv, glVertexAttrib4f, glVertexAttrib4fv, 
+    glVertexAttrib4iv, glVertexAttrib4s, glVertexAttrib4sv, 
+    glVertexAttrib4ubv, glVertexAttrib4uiv, glVertexAttrib4usv, 
+    glVertexAttribI1i, glVertexAttribI1iv, glVertexAttribI1ui, 
+    glVertexAttribI1uiv, glVertexAttribI2i, glVertexAttribI2iv, 
+    glVertexAttribI2ui, glVertexAttribI2uiv, glVertexAttribI3i, 
+    glVertexAttribI3iv, glVertexAttribI3ui, glVertexAttribI3uiv, 
+    glVertexAttribI4bv, glVertexAttribI4i, glVertexAttribI4iv, 
+    glVertexAttribI4sv, glVertexAttribI4ubv, glVertexAttribI4ui, 
+    glVertexAttribI4uiv, glVertexAttribI4usv, glVertexPointer, glWindowPos2d, 
+    glWindowPos2dv, glWindowPos2f, glWindowPos2fv, glWindowPos2i, 
+    glWindowPos2iv, glWindowPos2s, glWindowPos2sv, glWindowPos3d, 
+    glWindowPos3dv, glWindowPos3f, glWindowPos3fv, glWindowPos3i, 
+    glWindowPos3iv, glWindowPos3s, glWindowPos3sv, 
+    """
+
+
+_inject()
diff --git a/vispy/gloo/gl/pyopengl.py b/vispy/gloo/gl/pyopengl2.py
similarity index 57%
rename from vispy/gloo/gl/pyopengl.py
rename to vispy/gloo/gl/pyopengl2.py
index 5b22840..b0718e2 100644
--- a/vispy/gloo/gl/pyopengl.py
+++ b/vispy/gloo/gl/pyopengl2.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ GL ES 2.0 API implemented via pyOpenGL library. Intended as a
@@ -15,49 +15,6 @@ from . import _copy_gl_functions
 from ._constants import *  # noqa
 
 
-## Compatibility
-
-
-def glShaderSource_compat(handle, code):
-    """ This version of glShaderSource applies small modifications
-    to the given GLSL code in order to make it more compatible between
-    desktop and ES2.0 implementations. Specifically:
-      * It sets the #version pragma (if none is given already)
-      * It returns a (possibly empty) set of enums that should be enabled
-        (for automatically enabling point sprites)
-    """
-
-    # Make a string
-    if isinstance(code, (list, tuple)):
-        code = '\n'.join(code)
-
-    # Determine whether this is a vertex or fragment shader
-    code_ = '\n' + code
-    is_vertex = '\nattribute' in code_
-    is_fragment = not is_vertex
-
-    # Determine whether to write the #version pragma
-    write_version = True
-    for line in code.splitlines():
-        if line.startswith('#version'):
-            write_version = False
-            logger.warning('For compatibility accross different GL backends, '
-                           'avoid using the #version pragma.')
-    if write_version:
-        code = '#version 120\n#line 0\n' + code
-
-    # Do the call
-    glShaderSource(handle, [code])
-
-    # Determine whether to activate point sprites
-    enums = set()
-    if is_fragment and 'gl_PointCoord' in code:
-        enums.add(Enum('GL_VERTEX_PROGRAM_POINT_SIZE', 34370))
-        enums.add(Enum('GL_POINT_SPRITE', 34913))
-    return enums
-    return []
-
-
 def _patch():
     """ Monkey-patch pyopengl to fix a bug in glBufferSubData. """
     import sys
@@ -76,7 +33,6 @@ def _patch():
     except Exception:
         pass
 
-
 # Patch OpenGL package
 _patch()
 
@@ -84,7 +40,7 @@ _patch()
 ## Inject
 
 def _make_unavailable_func(funcname):
-    def cb(*args, **kwds):
+    def cb(*args, **kwargs):
         raise RuntimeError('OpenGL API call "%s" is not available.' % funcname)
     return cb
 
@@ -126,17 +82,17 @@ def _get_function_from_pyopengl(funcname):
 def _inject():
     """ Copy functions from OpenGL.GL into _pyopengl namespace.
     """
-    NS = _pyopengl.__dict__
-    for glname, ourname in _pyopengl._functions_to_import:
+    NS = _pyopengl2.__dict__
+    for glname, ourname in _pyopengl2._functions_to_import:
         func = _get_function_from_pyopengl(glname)
         NS[ourname] = func
 
 
-from . import _pyopengl
+from . import _pyopengl2  # noqa
 
 # Inject remaining functions from OpenGL.GL
-# copies name to _pyopengl namespace
+# copies name to _pyopengl2 namespace
 _inject()
 
-# Inject all function definitions in _pyopengl
-_copy_gl_functions(_pyopengl, globals())
+# Inject all function definitions in _pyopengl2
+_copy_gl_functions(_pyopengl2, globals())
diff --git a/vispy/gloo/gl/tests/test_basics.py b/vispy/gloo/gl/tests/test_basics.py
index 7d842e3..cee5952 100644
--- a/vispy/gloo/gl/tests/test_basics.py
+++ b/vispy/gloo/gl/tests/test_basics.py
@@ -7,12 +7,10 @@ The only exception is glCompressedTexImage2D and glCompressedTexSubImage2D.
 
 import sys
 
-from nose.tools import assert_equal, assert_true  # noqa
-
 from vispy.app import Canvas
 from numpy.testing import assert_almost_equal
 from vispy.testing import (requires_application, requires_pyopengl, SkipTest,
-                           glut_skip)
+                           run_tests_if_main, assert_equal, assert_true)
 from vispy.ext.six import string_types
 from vispy.util import use_log_level
 from vispy.gloo import gl
@@ -25,36 +23,31 @@ def teardown_module():
 @requires_application()
 def test_basics_desktop():
     """ Test desktop GL backend for basic functionality. """
-    glut_skip()
-    _test_basics('desktop')
+    _test_basics('gl2')
 
 
 @requires_application()
 def test_functionality_proxy():
     """ Test GL proxy class for basic functionality. """
     # By using debug mode, we are using the proxy class
-    glut_skip()
-    _test_basics('desktop debug')
+    _test_basics('gl2 debug')
 
 
 @requires_application()
 @requires_pyopengl()
 def test_basics_pypengl():
     """ Test pyopengl GL backend for basic functionality. """
-    glut_skip()
-    _test_basics('pyopengl')
+    _test_basics('pyopengl2')
 
 
 @requires_application()
-def test_functionality_angle():
-    """ Test angle GL backend for basic functionality. """
+def test_functionality_es2():
+    """ Test es2 GL backend for basic functionality. """
     if True:
-        raise SkipTest('Skip Angle functionality test for now.')
+        raise SkipTest('Skip es2 functionality test for now.')
     if sys.platform.startswith('win'):
-        raise SkipTest('Can only test angle functionality on Windows.')
-
-    glut_skip()
-    _test_basics('angle')
+        raise SkipTest('Can only test es2 functionality on Windows.')
+    _test_basics('es2')
 
 
 def _test_basics(backend):
@@ -91,7 +84,7 @@ def _test_setting_parameters():
     val = 0.2, 0.3
     gl.glDepthRange(*val)
     assert_almost_equal(gl.glGetParameter(gl.GL_DEPTH_RANGE), val)
-    
+
     gl.check_error()
 
 
@@ -110,7 +103,7 @@ def _test_enabling_disabling():
     gl.glDisable(gl.GL_BLEND)
     assert_equal(gl.glIsEnabled(gl.GL_BLEND), False)
     assert_equal(gl.glGetParameter(gl.GL_BLEND), 0)
-    
+
     gl.check_error()
 
 
@@ -263,8 +256,9 @@ def _test_fbo():
     assert_equal(width, w)
     
     # Touch copy tex functions
+    gl.glBindTexture(gl.GL_TEXTURE_2D, htex)
+    gl.glCopyTexSubImage2D(gl.GL_TEXTURE_2D, 0, 5, 5, 5, 5, 20, 20)
     gl.glCopyTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, 0, 0, 30, 30,  0)
-    gl.glCopyTexSubImage2D(gl.GL_TEXTURE_2D, 0,  20, 20,  0, 0, 10, 10)
     
     gl.check_error()
     
@@ -274,3 +268,6 @@ def _test_fbo():
     gl.glDeleteFramebuffer(hframebuf)
     
     gl.check_error()
+
+
+run_tests_if_main()
diff --git a/vispy/gloo/gl/tests/test_functionality.py b/vispy/gloo/gl/tests/test_functionality.py
index 156c656..4b64f3e 100644
--- a/vispy/gloo/gl/tests/test_functionality.py
+++ b/vispy/gloo/gl/tests/test_functionality.py
@@ -12,18 +12,15 @@ and each quadrant is drawn a different color (black, red, green,
 blue). The drawing is done for 50% using attribute data, and 50%
 using a texture. The end result should be fully saturated colors.
 
-Remember: the bottom left is (-1, -1) and the first quadrant. 
-
+Remember: the bottom left is (-1, -1) and the first quadrant.
 """
 import sys
 
 import numpy as np
 
-from nose.tools import assert_equal, assert_true
 from vispy.app import Canvas
-from numpy.testing import assert_almost_equal  # noqa
 from vispy.testing import (requires_application, requires_pyopengl, SkipTest,
-                           glut_skip)
+                           run_tests_if_main, assert_equal, assert_true)
 
 from vispy.gloo import gl
 
@@ -39,35 +36,31 @@ def teardown_module():
 @requires_application()
 def test_functionality_desktop():
     """ Test desktop GL backend for full functionality. """
-    glut_skip()
-    _test_functonality('desktop')
+    _test_functonality('gl2')
 
 
 @requires_application()
 def test_functionality_proxy():
     """ Test GL proxy class for full functionality. """
     # By using debug mode, we are using the proxy class
-    glut_skip()
-    _test_functonality('desktop debug')
+    _test_functonality('gl2 debug')
 
 
 @requires_application()
 @requires_pyopengl()
 def test_functionality_pyopengl():
     """ Test pyopengl GL backend for full functionality. """
-    glut_skip()
-    _test_functonality('pyopengl')
+    _test_functonality('pyopengl2')
 
 
 @requires_application()
-def test_functionality_angle():
-    """ Test angle GL backend for full functionality. """
+def test_functionality_es2():
+    """ Test es2 GL backend for full functionality. """
     if True:
-        raise SkipTest('Skip Angle functionality test for now.')
-    if sys.platform.startswith('win'):
-        raise SkipTest('Can only test angle functionality on Windows.')
-    glut_skip()
-    _test_functonality('angle')
+        raise SkipTest('Skip es2 functionality test for now.')
+    if not sys.platform.startswith('win'):
+        raise SkipTest('Can only test es2 functionality on Windows.')
+    _test_functonality('es2')
 
 
 def _clear_screen():
@@ -209,7 +202,6 @@ helements = None  # the OpenGL object ref
 
 ## The GL calls
 
-
 def _prepare_vis():
     
     objects = []
@@ -225,8 +217,8 @@ def _prepare_vis():
     objects.append((gl.glDeleteShader, hfrag))
     
     # Compile source code
-    gl.glShaderSource_compat(hvert, VERT)
-    gl.glShaderSource_compat(hfrag, FRAG)
+    gl.glShaderSource(hvert, VERT)
+    gl.glShaderSource(hfrag, FRAG)
     gl.glCompileShader(hvert)
     gl.glCompileShader(hfrag)
     
@@ -551,7 +543,4 @@ def _check_result(assert_result=True):
         assert_equal(pix4, (0, 0, 255))
 
 
-if __name__ == '__main__':
-    test_functionality_desktop()
-    test_functionality_pyopengl()
-    test_functionality_proxy()
+run_tests_if_main()
diff --git a/vispy/gloo/gl/tests/test_names.py b/vispy/gloo/gl/tests/test_names.py
index bf08d0b..f4ebb55 100644
--- a/vispy/gloo/gl/tests/test_names.py
+++ b/vispy/gloo/gl/tests/test_names.py
@@ -2,14 +2,18 @@
 backends, and no more than that.
 """
 
-from nose.tools import assert_equal
 from vispy.testing import requires_pyopengl
 
 from vispy.gloo import gl
+from vispy.testing import run_tests_if_main
+
+
+def teardown_module():
+    gl.use_gl()  # Reset to default
 
 
 class _DummyObject:
-    """ To be able to import angle even in Linux, so that we can test the
+    """ To be able to import es2 even in Linux, so that we can test the
     names defined inside.
     """
     def LoadLibrary(self, fname):
@@ -21,64 +25,73 @@ class _DummyObject:
 
 
 def _test_function_names(mod):
+    # The .difference(['gl2']) is to allow the gl2 module name
     fnames = set([name for name in dir(mod) if name.startswith('gl')])
-    assert_equal(function_names.difference(fnames), set())
-    assert_equal(fnames.difference(function_names), set())
+    assert function_names.difference(fnames) == set()
+    assert fnames.difference(function_names).difference(ok_names) == set()
 
 
-def _test_contant_names(mod):
+def _test_constant_names(mod):
     cnames = set([name for name in dir(mod) if name.startswith('GL')])
-    assert_equal(constant_names.difference(cnames), set())
-    assert_equal(cnames.difference(constant_names), set())
+    assert constant_names.difference(cnames) == set()
+    assert cnames.difference(constant_names) == set()
 
 
 def test_destop():
     """ Desktop backend should have all ES 2.0 names. No more, no less. """
-    from vispy.gloo.gl import desktop
-    _test_function_names(desktop)
-    _test_contant_names(desktop)
+    from vispy.gloo.gl import gl2
+    _test_function_names(gl2)
+    _test_constant_names(gl2)
 
 
-def test_angle():
-    """ Angle backend should have all ES 2.0 names. No more, no less. """
-    # Import. Install a dummy lib so that at least we can import angle.
+def test_es2():
+    """ es2 backend should have all ES 2.0 names. No more, no less. """
+    # Import. Install a dummy lib so that at least we can import es2.
     try:
-        from vispy.gloo.gl import angle  # noqa
+        from vispy.gloo.gl import es2  # noqa
     except Exception:
         import ctypes
-        ctypes.windll = _DummyObject()
-    from vispy.gloo.gl import angle  # noqa
+        ctypes.TEST_DLL = _DummyObject()
+    from vispy.gloo.gl import es2  # noqa
 
     # Test
-    _test_function_names(angle)
-    _test_contant_names(angle)
+    _test_function_names(es2)
+    _test_constant_names(es2)
 
 
 @requires_pyopengl()
 def test_pyopengl():
     """ Pyopengl backend should have all ES 2.0 names. No more, no less. """
-    from vispy.gloo.gl import pyopengl
-    _test_function_names(pyopengl)
-    _test_contant_names(pyopengl)
+    from vispy.gloo.gl import pyopengl2
+    _test_function_names(pyopengl2)
+    _test_constant_names(pyopengl2)
+
+
+ at requires_pyopengl()
+def test_glplus():
+    """ Run glplus, check that mo names, set back, check exact set of names.
+    """
+    gl.use_gl('gl+')
+    # Check that there are more names
+    fnames = set([name for name in dir(gl) if name.startswith('gl')])
+    assert len(fnames.difference(function_names).difference(['gl2'])) > 50
+    cnames = set([name for name in dir(gl) if name.startswith('GL')])
+    assert len(cnames.difference(constant_names)) > 50
+    gl.use_gl('gl2')
+    _test_function_names(gl)
+    _test_constant_names(gl)
 
 
 def test_proxy():
     """ GLProxy class should have all ES 2.0 names. No more, no less. """
     _test_function_names(gl.proxy)
-    _test_contant_names(gl._constants)
+    _test_constant_names(gl._constants)
 
 
 def test_main():
     """ Main gl namespace should have all ES 2.0 names. No more, no less. """
     _test_function_names(gl)
-    _test_contant_names(gl)
-
-
-def test_webgl():
-    """ Webgl backend should have all ES 2.0 names. No more, no less. """
-    from vispy.gloo.gl import webgl
-    _test_function_names(webgl)
-    _test_contant_names(webgl)
+    _test_constant_names(gl)
 
 
 def _main():
@@ -87,9 +100,8 @@ def _main():
     test_main()
     test_proxy()
     test_destop()
-    test_angle()
+    test_es2()
     test_pyopengl()
-    test_webgl()
 
 
 # Note: I took these names below from _main and _constants, which is a
@@ -119,7 +131,7 @@ glGetVertexAttribOffset glHint glIsBuffer glIsEnabled glIsFramebuffer
 glIsProgram glIsRenderbuffer glIsShader glIsTexture glLineWidth
 glLinkProgram glPixelStorei glPolygonOffset glReadPixels
 glRenderbufferStorage glSampleCoverage glScissor glShaderSource
-glShaderSource_compat glStencilFunc glStencilFuncSeparate glStencilMask
+glStencilFunc glStencilFuncSeparate glStencilMask
 glStencilMaskSeparate glStencilOp glStencilOpSeparate glTexImage2D
 glTexParameterf glTexParameteri glTexSubImage2D glUniform1f glUniform1fv
 glUniform1i glUniform1iv glUniform2f glUniform2fv glUniform2i
@@ -227,6 +239,6 @@ function_names = [n.strip() for n in function_names.split(' ')]
 function_names = set([n for n in function_names if n])
 constant_names = [n.strip() for n in constant_names.split(' ')]
 constant_names = set([n for n in constant_names if n])
+ok_names = set(['gl2', 'glplus'])  # module names
 
-if __name__ == '__main__':
-    _main()
+run_tests_if_main()
diff --git a/vispy/gloo/gl/tests/test_use.py b/vispy/gloo/gl/tests/test_use.py
index 8cd79f3..0d53c82 100644
--- a/vispy/gloo/gl/tests/test_use.py
+++ b/vispy/gloo/gl/tests/test_use.py
@@ -4,6 +4,7 @@
 from vispy.testing import assert_is, requires_pyopengl
 
 from vispy.gloo import gl
+from vispy.testing import run_tests_if_main
 
 
 def teardown_module():
@@ -15,44 +16,58 @@ def test_use_desktop():
     """ Testing that gl.use injects all names in gl namespace """
 
     # Use desktop
-    gl.use_gl('desktop')
+    gl.use_gl('gl2')
     #
-    for name in dir(gl.desktop):
+    for name in dir(gl.gl2):
         if name.lower().startswith('gl'):
             val1 = getattr(gl, name)
-            val2 = getattr(gl.desktop, name)
+            val2 = getattr(gl.gl2, name)
             assert_is(val1, val2)
 
     # Use pyopengl
-    gl.use_gl('pyopengl')
+    gl.use_gl('pyopengl2')
     #
-    for name in dir(gl.desktop):
+    for name in dir(gl.gl2):
         if name.lower().startswith('gl'):
             val1 = getattr(gl, name)
-            val2 = getattr(gl.pyopengl, name)
+            val2 = getattr(gl.pyopengl2, name)
             assert_is(val1, val2)
     
-    # Use webgl
-    gl.use_gl('webgl')
+    # Use gl+ 
+    gl.use_gl('gl+')
+    # uses all ES2 names from pyopengl2 backend
+    for name in dir(gl.gl2):
+        if name.lower().startswith('gl'):
+            val1 = getattr(gl, name)
+            val2 = getattr(gl.pyopengl2, name)
+            assert_is(val1, val2)
+    # But provides extra names too
+    for name in dir(gl.glplus):
+        if name.lower().startswith('gl'):
+            val1 = getattr(gl, name)
+            val2 = getattr(gl.glplus, name)
+            assert_is(val1, val2)
+    
+    # Use dummy
+    gl.use_gl('dummy')
     #
-    for name in dir(gl.desktop):
+    for name in dir(gl.gl2):
         if name.lower().startswith('gl'):
             val1 = getattr(gl, name)
-            val2 = getattr(gl.webgl, name)
+            val2 = getattr(gl.dummy, name)
             assert_is(val1, val2)
     
     # Touch debug wrapper stuff
-    gl.use_gl('desktop debug')
+    gl.use_gl('gl2 debug')
     
     # Use desktop again
-    gl.use_gl('desktop')
+    gl.use_gl('gl2')
     #
-    for name in dir(gl.desktop):
+    for name in dir(gl.gl2):
         if name.lower().startswith('gl'):
             val1 = getattr(gl, name)
-            val2 = getattr(gl.desktop, name)
+            val2 = getattr(gl.gl2, name)
             assert_is(val1, val2)
 
 
-if __name__ == '__main__':
-    test_use_desktop()
+run_tests_if_main()
diff --git a/vispy/gloo/gl/webgl.py b/vispy/gloo/gl/webgl.py
deleted file mode 100644
index 84e060e..0000000
--- a/vispy/gloo/gl/webgl.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-""" GL ES 2.0 API implemented via WebGL.
-"""
-
-from . import BaseGLProxy, _copy_gl_functions
-from ._constants import *  # noqa
-
-
-class WebGLProxy(BaseGLProxy):
-    """ Dummy proxy class for WebGL. More or less psuedo code for now :)
-    But this should get whomever is going to work on WebGL a good place to
-    start.
-    Note that in order to use WebGL, we also need a WebGL app, probably
-    also via some sort of proxy class. 
-    """
-    
-    def __call__(self, funcname, returns, *args):
-        
-        self.websocket.send(funcname, *args)
-        if returns:
-            return self.websocket.wait_for_result()
-
-
-# Instantiate proxy and inject functions
-_proxy = WebGLProxy()
-_copy_gl_functions(_proxy, globals())
diff --git a/vispy/gloo/glir.py b/vispy/gloo/glir.py
new file mode 100644
index 0000000..67419b5
--- /dev/null
+++ b/vispy/gloo/glir.py
@@ -0,0 +1,1266 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+""" 
+Implementation to execute GL Intermediate Representation (GLIR)
+"""
+
+import sys
+import re
+import json
+
+import numpy as np
+
+from . import gl
+from ..ext.six import string_types
+from ..util import logger
+
+# TODO: expose these via an extension space in .gl?
+_internalformats = [
+    gl.Enum('GL_RED', 6403),
+    gl.Enum('GL_R', 8194), 
+    gl.Enum('GL_R8', 33321), 
+    gl.Enum('GL_R16', 33322), 
+    gl.Enum('GL_R16F', 33325), 
+    gl.Enum('GL_R32F', 33326),
+    gl.Enum('GL_RG', 33319), 
+    gl.Enum('GL_RG8', 333323),
+    gl.Enum('GL_RG16', 333324),
+    gl.Enum('GL_RG16F', 333327),
+    gl.Enum('GL_RG32F', 33328),
+    gl.Enum('GL_RGB', 6407),
+    gl.Enum('GL_RGB8', 32849),
+    gl.Enum('GL_RGB16', 32852),
+    gl.Enum('GL_RGB16F', 34843),
+    gl.Enum('GL_RGB32F', 34837),
+    gl.Enum('GL_RGBA', 6408),
+    gl.Enum('GL_RGBA8', 32856),
+    gl.Enum('GL_RGBA16', 32859),
+    gl.Enum('GL_RGBA16F', 34842),
+    gl.Enum('GL_RGBA32F', 34836)
+]
+_internalformats = dict([(enum.name, enum) for enum in _internalformats])
+
+# Value to mark a glir object that was just deleted. So we can safely
+# ignore it (and not raise an error that the object could not be found).
+# This can happen e.g. if A is created, A is bound to B and then A gets
+# deleted. The commands may get executed in order: A gets created, A
+# gets deleted, A gets bound to B.
+JUST_DELETED = 'JUST_DELETED'
+
+
+def as_enum(enum):
+    """ Turn a possibly string enum into an integer enum.
+    """
+    if isinstance(enum, string_types):
+        try:
+            enum = getattr(gl, 'GL_' + enum.upper())
+        except AttributeError:
+            try:
+                enum = _internalformats['GL_' + enum.upper()]
+            except KeyError:
+                raise ValueError('Could not find int value for enum %r' % enum)
+    return enum
+
+
+class GlirQueue(object):
+    """ Representation of a queue of GLIR commands
+    
+    One instance of this class is attached to each context object, and
+    to each gloo object.
+    
+    Upon drawing (i.e. `Program.draw()`) and framebuffer switching, the
+    commands in the queue are pushed to a parser, which is stored at
+    context.shared. The parser can interpret the commands in Python,
+    send them to a browser, etc.
+    """
+    
+    def __init__(self):
+        self._commands = []  # local commands
+        self._verbose = False
+        self._associations = set()
+    
+    def command(self, *args):
+        """ Send a command. See the command spec at:
+        https://github.com/vispy/vispy/wiki/Spec.-Gloo-IR
+        """
+        self._commands.append(args)
+    
+    def set_verbose(self, verbose):
+        """ Set verbose or not. If True, the GLIR commands are printed
+        right before they get parsed. If a string is given, use it as
+        a filter.
+        """
+        self._verbose = verbose
+    
+    def show(self, filter=None):
+        """ Print the list of commands currently in the queue. If filter is
+        given, print only commands that match the filter.
+        """
+        # Show commands in associated queues
+        for q in self._associations:
+            q.show()
+        
+        for command in self._commands:
+            if command[0] is None:  # or command[1] in self._invalid_objects:
+                continue  # Skip nill commands 
+            if filter and command[0] != filter:
+                continue
+            t = []
+            for e in command:
+                if isinstance(e, np.ndarray):
+                    t.append('array %s' % str(e.shape))
+                elif isinstance(e, str):
+                    s = e.strip()
+                    if len(s) > 20:
+                        s = s[:18] + '... %i lines' % (e.count('\n')+1)
+                    t.append(s)
+                else:
+                    t.append(e)
+            print(tuple(t))
+    
+    def clear(self):
+        """ Pop the whole queue (and associated queues) and return a
+        list of commands.
+        """
+        
+        # Get all commands, discard deletable queues (ques no longer in use)
+        commands = []
+        for q in list(self._associations):
+            commands.extend(q.clear())
+            if hasattr(q, '_deletable'):  # this flag gets set by GLObject
+                self._associations.discard(q)
+        commands.extend(self._commands)
+        self._commands[:] = []
+        return commands
+    
+    def associate(self, queue):
+        """ Associate the given queue. When the current queue gets
+        cleared, it first clears all the associated queues and prepends
+        these commands to the total list. One should call associate()
+        on the queue that relies on the other 
+        (e.g. ``program.glir.associate(texture.glir``).
+        """
+        assert isinstance(queue, GlirQueue)
+        self._associations.add(queue)
+    
+    def flush(self, parser):
+        """ Flush all current commands to the GLIR interpreter.
+        """
+#         if self._parser is None:
+#             raise RuntimeError('Cannot flush queue if parser is None')
+        if self._verbose:
+            show = self._verbose if isinstance(self._verbose, str) else None
+            self.show(show)
+        parser.parse(self._filter(self.clear(), parser))
+    
+    def _filter(self, commands, parser):
+        """ Filter DATA/SIZE commands that are overridden by a 
+        SIZE command.
+        """
+        resized = set()
+        commands2 = []
+        for command in reversed(commands):
+            if command[0] == 'SHADERS':
+                convert = parser.convert_shaders()
+                if convert:
+                    shaders = self._convert_shaders(convert, command[2:])
+                    command = command[:2] + shaders
+            elif command[1] in resized:
+                if command[0] in ('SIZE', 'DATA'):
+                    continue  # remove this command
+            elif command[0] == 'SIZE':
+                resized.add(command[1])
+            commands2.append(command)
+        return list(reversed(commands2))
+
+    def _convert_shaders(self, convert, shaders):
+        return convert_shaders(convert, shaders)
+
+
+def convert_shaders(convert, shaders):
+    """ Modify shading code so that we can write code once
+    and make it run "everywhere".
+    """
+        
+    # New version of the shaders
+    out = []
+    
+    if convert == 'es2':
+        
+        for isfragment, shader in enumerate(shaders):
+            has_version = False
+            has_prec_float = False
+            has_prec_int = False
+            lines = []
+            # Iterate over lines
+            for line in shader.lstrip().splitlines():
+                if line.startswith('#version'):
+                    has_version = True
+                    continue
+                if line.startswith('precision '):
+                    has_prec_float = has_prec_float or 'float' in line
+                    has_prec_int = has_prec_int or 'int' in line
+                lines.append(line.rstrip())
+            # Write
+            # BUG: fails on WebGL (Chrome)
+            # if True:
+            #     lines.insert(has_version, '#line 0')
+            if not has_prec_float:
+                lines.insert(has_version, 'precision highp float;')
+            if not has_prec_int:
+                lines.insert(has_version, 'precision highp int;')
+            # BUG: fails on WebGL (Chrome)
+            # if not has_version:
+            #     lines.insert(has_version, '#version 100')
+            out.append('\n'.join(lines))
+    
+    elif convert == 'desktop':
+        
+        for isfragment, shader in enumerate(shaders):
+            has_version = False
+            lines = []
+            # Iterate over lines
+            for line in shader.lstrip().splitlines():
+                has_version = has_version or line.startswith('#version')
+                if line.startswith('precision '):
+                    line = ''
+                for prec in (' highp ', ' mediump ', ' lowp '):
+                    line = line.replace(prec, ' ')
+                lines.append(line.rstrip())
+            # Write
+            if not has_version:
+                lines.insert(0, '#version 120\n#line 2\n')
+            out.append('\n'.join(lines))
+    
+    else:
+        raise ValueError('Cannot convert shaders to %r.' % convert)
+        
+    return tuple(out)
+
+
+def as_es2_command(command):
+    """ Modify a desktop command so it works on es2.
+    """
+
+    if command[0] == 'FUNC':
+        return (command[0], re.sub(r'^gl([A-Z])',
+                lambda m: m.group(1).lower(), command[1])) + command[2:]
+    if command[0] == 'SHADERS':
+        return command[:2] + convert_shaders('es2', command[2:])
+    if command[0] == 'UNIFORM':
+        return command[:-1] + (command[-1].tolist(),)
+    return command
+
+
+class BaseGlirParser(object):
+    """ Base clas for GLIR parsers that can be attached to a GLIR queue.
+    """
+    
+    def is_remote(self):
+        """ Whether the code is executed remotely. i.e. gloo.gl cannot
+        be used.
+        """
+        raise NotImplementedError()
+    
+    def convert_shaders(self):
+        """ Whether to convert shading code. Valid values are 'es2' and
+        'desktop'. If None, the shaders are not modified.
+        """
+        raise NotImplementedError()
+    
+    def parse(self, commands):
+        """ Parse the GLIR commands. Or sent them away.
+        """
+        raise NotImplementedError()
+
+
+class GlirParser(BaseGlirParser):
+    """ A class for interpreting GLIR commands using gloo.gl
+    
+    We make use of relatively light GLIR objects that are instantiated
+    on CREATE commands. These objects are stored by their id in a
+    dictionary so that commands like ACTIVATE and DATA can easily
+    be executed on the corresponding objects.
+    """
+    
+    def __init__(self):
+        self._objects = {}
+        self._invalid_objects = set()
+        
+        self._classmap = {'Program': GlirProgram,
+                          'VertexBuffer': GlirVertexBuffer,
+                          'IndexBuffer': GlirIndexBuffer,
+                          'Texture1D': GlirTexture1D,
+                          'Texture2D': GlirTexture2D,
+                          'Texture3D': GlirTexture3D,
+                          'RenderBuffer': GlirRenderBuffer,
+                          'FrameBuffer': GlirFrameBuffer,
+                          }
+        
+        # We keep a dict that the GLIR objects use for storing
+        # per-context information. This dict is cleared each time
+        # that the context is made current. This seems necessary for
+        # when two Canvases share a context.
+        self.env = {}
+    
+    def is_remote(self):
+        return False
+    
+    def convert_shaders(self):
+        if '.es' in gl.current_backend.__name__:
+            return 'es2'
+        else:
+            return 'desktop'
+
+    def _parse(self, command):
+        """ Parse a single command.
+        """
+
+        cmd, id_, args = command[0], command[1], command[2:]
+            
+        if cmd == 'CURRENT':
+            # This context is made current
+            self.env.clear()
+            self._gl_initialize()
+            gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
+        elif cmd == 'FUNC':
+            # GL function call
+            args = [as_enum(a) for a in args]
+            try:
+                getattr(gl, id_)(*args)
+            except AttributeError:
+                logger.warning('Invalid gl command: %r' % id_)
+        elif cmd == 'CREATE':
+            # Creating an object
+            if args[0] is not None:
+                klass = self._classmap[args[0]]
+                self._objects[id_] = klass(self, id_)
+            else:
+                self._invalid_objects.add(id_)
+        elif cmd == 'DELETE':
+            # Deleting an object
+            ob = self._objects.get(id_, None)
+            if ob is not None:
+                self._objects[id_] = JUST_DELETED
+                ob.delete()
+        else:
+            # Doing somthing to an object
+            ob = self._objects.get(id_, None)
+            if ob == JUST_DELETED:
+                return
+            if ob is None:
+                if id_ not in self._invalid_objects:
+                    raise RuntimeError('Cannot %s object %i because it '
+                                       'does not exist' % (cmd, id_))
+                return
+            # Triage over command. Order of commands is set so most
+            # common ones occur first.
+            if cmd == 'DRAW':  # Program
+                ob.draw(*args)
+            elif cmd == 'TEXTURE':  # Program
+                ob.set_texture(*args)
+            elif cmd == 'UNIFORM':  # Program
+                ob.set_uniform(*args)
+            elif cmd == 'ATTRIBUTE':  # Program
+                ob.set_attribute(*args)
+            elif cmd == 'DATA':  # VertexBuffer, IndexBuffer, Texture
+                ob.set_data(*args)
+            elif cmd == 'SIZE':  # VertexBuffer, IndexBuffer,
+                ob.set_size(*args)  # Texture[1D, 2D, 3D], RenderBuffer
+            elif cmd == 'ATTACH':  # FrameBuffer
+                ob.attach(*args)
+            elif cmd == 'FRAMEBUFFER':  # FrameBuffer
+                ob.set_framebuffer(*args)
+            elif cmd == 'SHADERS':  # Program
+                ob.set_shaders(*args)
+            elif cmd == 'WRAPPING':  # Texture1D, Texture2D, Texture3D
+                ob.set_wrapping(*args)
+            elif cmd == 'INTERPOLATION':  # Texture1D, Texture2D, Texture3D
+                ob.set_interpolation(*args)
+            else:
+                logger.warning('Invalid GLIR command %r' % cmd)
+   
+    def parse(self, commands):
+        """ Parse a list of commands.
+        """
+        
+        # Get rid of dummy objects that represented deleted objects in
+        # the last parsing round.
+        to_delete = []
+        for id_, val in self._objects.items():
+            if val == JUST_DELETED:
+                to_delete.append(id_)
+        for id_ in to_delete:
+            self._objects.pop(id_)
+        
+        for command in commands:
+            self._parse(command)
+
+    def get_object(self, id_):
+        """ Get the object with the given id or None if it does not exist.
+        """
+        return self._objects.get(id_, None)
+    
+    def _gl_initialize(self):
+        """ Deal with compatibility; desktop does not have sprites
+        enabled by default. ES has.
+        """
+        if '.es' in gl.current_backend.__name__:
+            pass  # ES2: no action required
+        else:
+            # Desktop, enable sprites
+            GL_VERTEX_PROGRAM_POINT_SIZE = 34370
+            GL_POINT_SPRITE = 34913
+            gl.glEnable(GL_VERTEX_PROGRAM_POINT_SIZE)
+            gl.glEnable(GL_POINT_SPRITE)
+
+
+def glir_logger(parser_cls, file_or_filename):
+    from ..util.logs import NumPyJSONEncoder
+
+    class cls(parser_cls):
+        def __init__(self, *args, **kwargs):
+            parser_cls.__init__(self, *args, **kwargs)
+
+            if isinstance(file_or_filename, string_types):
+                self._file = open(file_or_filename, 'w')
+            else:
+                self._file = file_or_filename
+
+            self._file.write('[]')
+            self._empty = True
+
+        def _parse(self, command):
+            parser_cls._parse(self, command)
+
+            self._file.seek(self._file.tell() - 1)
+            if self._empty:
+                self._empty = False
+            else:
+                self._file.write(',\n')
+            json.dump(as_es2_command(command),
+                      self._file, cls=NumPyJSONEncoder)
+            self._file.write(']')
+
+    return cls
+
+
+## GLIR objects
+
+class GlirObject(object):
+    def __init__(self, parser, id_):
+        self._parser = parser
+        self._id = id_
+        self._handle = -1  # Must be set by subclass in create()
+        self.create()
+    
+    @property
+    def handle(self):
+        return self._handle
+    
+    @property
+    def id(self):
+        return self._id
+    
+    def __repr__(self):
+        return '<%s %i at 0x%x>' % (self.__class__.__name__, self.id, id(self))
+
+
+class GlirProgram(GlirObject):
+    
+    UTYPEMAP = {
+        'float': 'glUniform1fv',
+        'vec2': 'glUniform2fv',
+        'vec3': 'glUniform3fv',
+        'vec4': 'glUniform4fv',
+        'int': 'glUniform1iv',
+        'ivec2': 'glUniform2iv',
+        'ivec3': 'glUniform3iv',
+        'ivec4': 'glUniform4iv',
+        'bool': 'glUniform1iv',
+        'bvec2': 'glUniform2iv',
+        'bvec3': 'glUniform3iv',
+        'bvec4': 'glUniform4iv',
+        'mat2': 'glUniformMatrix2fv',
+        'mat3': 'glUniformMatrix3fv',
+        'mat4': 'glUniformMatrix4fv',
+        'sampler1D': 'glUniform1i',
+        'sampler2D': 'glUniform1i',
+        'sampler3D': 'glUniform1i',
+    }
+    
+    ATYPEMAP = {
+        'float': 'glVertexAttrib1f',
+        'vec2': 'glVertexAttrib2f',
+        'vec3': 'glVertexAttrib3f',
+        'vec4': 'glVertexAttrib4f',
+    }
+    
+    ATYPEINFO = {
+        'float': (1, gl.GL_FLOAT, np.float32),
+        'vec2': (2, gl.GL_FLOAT, np.float32),
+        'vec3': (3, gl.GL_FLOAT, np.float32),
+        'vec4': (4, gl.GL_FLOAT, np.float32),
+        'int': (1, gl.GL_INT, np.int32),
+    }
+    
+    def create(self):
+        self._handle = gl.glCreateProgram()
+        self._validated = False
+        self._linked = False
+        # Keeping track of uniforms/attributes
+        self._handles = {}  # cache with handles to attributes/uniforms
+        self._unset_variables = set()
+        # Store samplers in buffers that are bount to uniforms/attributes
+        self._samplers = {}  # name -> (tex-target, tex-handle, unit)
+        self._attributes = {}  # name -> (vbo-handle, attr-handle, func, args)
+        self._known_invalid = set()  # variables that we know are invalid
+    
+    def delete(self):
+        gl.glDeleteProgram(self._handle)
+    
+    def activate(self):
+        """ Avoid overhead in calling glUseProgram with same arg.
+        Warning: this will break if glUseProgram is used somewhere else.
+        Per context we keep track of one current program.
+        """
+        if self._handle != self._parser.env.get('current_program', False):
+            self._parser.env['current_program'] = self._handle
+            gl.glUseProgram(self._handle)
+    
+    def deactivate(self):
+        """ Avoid overhead in calling glUseProgram with same arg.
+        Warning: this will break if glUseProgram is used somewhere else.
+        Per context we keep track of one current program.
+        """
+        if self._parser.env.get('current_program', 0) != 0:
+            self._parser.env['current_program'] = 0
+            gl.glUseProgram(0)
+    
+    def set_shaders(self, vert, frag):
+        """ This function takes care of setting the shading code and
+        compiling+linking it into a working program object that is ready
+        to use.
+        """
+        self._linked = False
+        # Create temporary shader objects
+        vert_handle = gl.glCreateShader(gl.GL_VERTEX_SHADER)
+        frag_handle = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
+        # For both vertex and fragment shader: set source, compile, check
+        for code, handle, type_ in [(vert, vert_handle, 'vertex'), 
+                                    (frag, frag_handle, 'fragment')]:
+            gl.glShaderSource(handle, code)
+            gl.glCompileShader(handle)
+            status = gl.glGetShaderParameter(handle, gl.GL_COMPILE_STATUS)
+            if not status:
+                errors = gl.glGetShaderInfoLog(handle)
+                errormsg = self._get_error(code, errors, 4)
+                raise RuntimeError("Shader compilation error in %s:\n%s" % 
+                                   (type_ + ' shader', errormsg))
+        # Attach shaders
+        gl.glAttachShader(self._handle, vert_handle)
+        gl.glAttachShader(self._handle, frag_handle)
+        # Link the program and check
+        gl.glLinkProgram(self._handle)
+        if not gl.glGetProgramParameter(self._handle, gl.GL_LINK_STATUS):
+            print(gl.glGetProgramInfoLog(self._handle))
+            raise RuntimeError('Program linking error')
+        # Now we can remove the shaders. We no longer need them and it
+        # frees up precious GPU memory:
+        # http://gamedev.stackexchange.com/questions/47910
+        gl.glDetachShader(self._handle, vert_handle)
+        gl.glDetachShader(self._handle, frag_handle)
+        gl.glDeleteShader(vert_handle)
+        gl.glDeleteShader(frag_handle)
+        # Now we know what variables will be used by the program
+        self._unset_variables = self._get_active_attributes_and_uniforms()
+        self._handles = {}
+        self._known_invalid = set()
+        self._linked = True
+        
+    def _get_active_attributes_and_uniforms(self):
+        """ Retrieve active attributes and uniforms to be able to check that
+        all uniforms/attributes are set by the user.
+        Other GLIR implementations may omit this.
+        """
+        # This match a name of the form "name[size]" (= array)
+        regex = re.compile("""(?P<name>\w+)\s*(\[(?P<size>\d+)\])\s*""")
+        # Get how many active attributes and uniforms there are
+        cu = gl.glGetProgramParameter(self._handle, gl.GL_ACTIVE_UNIFORMS)
+        ca = gl.glGetProgramParameter(self.handle, gl.GL_ACTIVE_ATTRIBUTES)
+        # Get info on each one
+        attributes = []
+        uniforms = []
+        for container, count, func in [(attributes, ca, gl.glGetActiveAttrib),
+                                       (uniforms, cu, gl.glGetActiveUniform)]:
+            for i in range(count):
+                name, size, gtype = func(self._handle, i)
+                m = regex.match(name)  # Check if xxx[0] instead of xx
+                if m:
+                    name = m.group('name')
+                    for i in range(size):
+                        container.append(('%s[%d]' % (name, i), gtype))
+                else:
+                    container.append((name, gtype))
+        #return attributes, uniforms
+        return set([v[0] for v in attributes] + [v[0] for v in uniforms])
+    
+    def _parse_error(self, error):
+        """ Parses a single GLSL error and extracts the linenr and description
+        Other GLIR implementations may omit this.
+        """
+        error = str(error)
+        # Nvidia
+        # 0(7): error C1008: undefined variable "MV"
+        m = re.match(r'(\d+)\((\d+)\)\s*:\s(.*)', error)
+        if m:
+            return int(m.group(2)), m.group(3)
+        # ATI / Intel
+        # ERROR: 0:131: '{' : syntax error parse error
+        m = re.match(r'ERROR:\s(\d+):(\d+):\s(.*)', error)
+        if m:
+            return int(m.group(2)), m.group(3)
+        # Nouveau
+        # 0:28(16): error: syntax error, unexpected ')', expecting '('
+        m = re.match(r'(\d+):(\d+)\((\d+)\):\s(.*)', error)
+        if m:
+            return int(m.group(2)), m.group(4)
+        # Other ...
+        return None, error
+
+    def _get_error(self, code, errors, indentation=0):
+        """Get error and show the faulty line + some context
+        Other GLIR implementations may omit this.
+        """
+        # Init
+        results = []
+        lines = None
+        if code is not None:
+            lines = [line.strip() for line in code.split('\n')]
+
+        for error in errors.split('\n'):
+            # Strip; skip empy lines
+            error = error.strip()
+            if not error:
+                continue
+            # Separate line number from description (if we can)
+            linenr, error = self._parse_error(error)
+            if None in (linenr, lines):
+                results.append('%s' % error)
+            else:
+                results.append('on line %i: %s' % (linenr, error))
+                if linenr > 0 and linenr < len(lines):
+                    results.append('  %s' % lines[linenr - 1])
+
+        # Add indentation and return
+        results = [' ' * indentation + r for r in results]
+        return '\n'.join(results)
+    
+    def set_texture(self, name, value):
+        """ Set a texture sampler. Value is the id of the texture to link.
+        """
+        if not self._linked:
+            raise RuntimeError('Cannot set uniform when program has no code')
+        # Get handle for the uniform, first try cache
+        handle = self._handles.get(name, -1)
+        if handle < 0:
+            if name in self._known_invalid:
+                return
+            handle = gl.glGetUniformLocation(self._handle, name)
+            self._unset_variables.discard(name)  # Mark as set
+            self._handles[name] = handle  # Store in cache
+            if handle < 0:
+                self._known_invalid.add(name)
+                logger.info('Variable %s is not an active uniform' % name)
+                return
+        # Program needs to be active in order to set uniforms
+        self.activate()
+        if True:
+            # Sampler: the value is the id of the texture
+            tex = self._parser.get_object(value)
+            if tex == JUST_DELETED:
+                return
+            if tex is None:
+                raise RuntimeError('Could not find texture with id %i' % value)
+            unit = len(self._samplers)
+            if name in self._samplers:
+                unit = self._samplers[name][-1]  # Use existing unit            
+            self._samplers[name] = tex._target, tex.handle, unit
+            gl.glUniform1i(handle, unit)
+
+    def set_uniform(self, name, type_, value):
+        """ Set a uniform value. Value is assumed to have been checked.
+        """
+        if not self._linked:
+            raise RuntimeError('Cannot set uniform when program has no code')
+        # Get handle for the uniform, first try cache
+        handle = self._handles.get(name, -1)
+        count = 1
+        if handle < 0:
+            if name in self._known_invalid:
+                return
+            handle = gl.glGetUniformLocation(self._handle, name)
+            self._unset_variables.discard(name)  # Mark as set
+            # if we set a uniform_array, mark all as set
+            if not type_.startswith('mat'):
+                count = value.nbytes // (4 * self.ATYPEINFO[type_][0])
+            if count > 1:
+                for ii in range(count):
+                    if '%s[%s]' % (name, ii) in self._unset_variables:
+                        self._unset_variables.discard('%s[%s]' % (name, ii))
+
+            self._handles[name] = handle  # Store in cache
+            if handle < 0:
+                self._known_invalid.add(name)
+                logger.info('Variable %s is not an active uniform' % name)
+                return
+        # Look up function to call
+        funcname = self.UTYPEMAP[type_]
+        func = getattr(gl, funcname)
+        # Program needs to be active in order to set uniforms
+        self.activate()
+        # Triage depending on type 
+        if type_.startswith('mat'):
+            # Value is matrix, these gl funcs have alternative signature
+            transpose = False  # OpenGL ES 2.0 does not support transpose
+            func(handle, 1, transpose, value)
+        else:
+            # Regular uniform
+            func(handle, count, value)
+    
+    def set_attribute(self, name, type_, value):
+        """ Set an attribute value. Value is assumed to have been checked.
+        """
+        if not self._linked:
+            raise RuntimeError('Cannot set attribute when program has no code')
+        # Get handle for the attribute, first try cache
+        handle = self._handles.get(name, -1)
+        if handle < 0:
+            if name in self._known_invalid:
+                return
+            handle = gl.glGetAttribLocation(self._handle, name)
+            self._unset_variables.discard(name)  # Mark as set
+            self._handles[name] = handle  # Store in cache
+            if handle < 0:
+                self._known_invalid.add(name)
+                if value[0] != 0 and value[2] > 0:  # VBO with offset
+                    return  # Probably an unused element in a structured VBO
+                logger.info('Variable %s is not an active attribute' % name)
+                return
+        # Program needs to be active in order to set uniforms
+        self.activate()
+        # Triage depending on VBO or tuple data
+        if value[0] == 0:
+            # Look up function call
+            funcname = self.ATYPEMAP[type_]
+            func = getattr(gl, funcname)
+            # Set data
+            self._attributes[name] = 0, handle, func, value[1:]
+        else:
+            # Get meta data
+            vbo_id, stride, offset = value
+            size, gtype, dtype = self.ATYPEINFO[type_]
+            # Get associated VBO
+            vbo = self._parser.get_object(vbo_id)
+            if vbo == JUST_DELETED:
+                return
+            if vbo is None:
+                raise RuntimeError('Could not find VBO with id %i' % vbo_id)
+            # Set data
+            func = gl.glVertexAttribPointer
+            args = size, gtype, gl.GL_FALSE, stride, offset
+            self._attributes[name] = vbo.handle, handle, func, args
+    
+    def _pre_draw(self):
+        self.activate()
+        # Activate textures
+        for tex_target, tex_handle, unit in self._samplers.values():
+            gl.glActiveTexture(gl.GL_TEXTURE0 + unit)
+            gl.glBindTexture(tex_target, tex_handle)
+        # Activate attributes
+        for vbo_handle, attr_handle, func, args in self._attributes.values():
+            if vbo_handle:
+                gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo_handle)
+                gl.glEnableVertexAttribArray(attr_handle)
+                func(attr_handle, *args)
+            else:
+                gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
+                gl.glDisableVertexAttribArray(attr_handle)
+                func(attr_handle, *args)
+        # Validate. We need to validate after textures units get assigned
+        if not self._validated:
+            self._validated = True
+            self._validate()
+    
+    def _validate(self):
+        # Validate ourselves
+        if self._unset_variables:
+            logger.info('Program has unset variables: %r' %
+                        self._unset_variables)
+        # Validate via OpenGL
+        gl.glValidateProgram(self._handle)
+        if not gl.glGetProgramParameter(self._handle, 
+                                        gl.GL_VALIDATE_STATUS):
+            print(gl.glGetProgramInfoLog(self._handle))
+            raise RuntimeError('Program validation error')
+    
+    def _post_draw(self):
+        # No need to deactivate each texture/buffer, just set to 0
+        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
+        gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
+        if USE_TEX_3D:
+            gl.glBindTexture(GL_TEXTURE_3D, 0)
+            gl.glBindTexture(GL_TEXTURE_1D, 0)
+
+        #Deactivate program - should not be necessary. In single-program
+        #apps it would not even make sense.
+        #self.deactivate()
+    
+    def draw(self, mode, selection):
+        """ Draw program in given mode, with given selection (IndexBuffer or
+        first, count).
+        """
+        if not self._linked:
+            raise RuntimeError('Cannot draw program if code has not been set')
+        # Init
+        gl.check_error('Check before draw')
+        mode = as_enum(mode)
+        # Draw
+        if len(selection) == 3:
+            # Selection based on indices
+            id_, gtype, count = selection
+            if count:
+                self._pre_draw()
+                ibuf = self._parser.get_object(id_)
+                ibuf.activate()
+                gl.glDrawElements(mode, count, as_enum(gtype), None)
+                ibuf.deactivate()
+        else:
+            # Selection based on start and count
+            first, count = selection
+            if count:
+                self._pre_draw()
+                gl.glDrawArrays(mode, first, count)
+        # Wrap up
+        gl.check_error('Check after draw')
+        self._post_draw()
+
+
+class GlirBuffer(GlirObject):
+    _target = None
+    _usage = gl.GL_DYNAMIC_DRAW  # STATIC_DRAW, STREAM_DRAW or DYNAMIC_DRAW
+    
+    def create(self):
+        self._handle = gl.glCreateBuffer()
+        self._buffer_size = 0
+        self._bufferSubDataOk = False
+    
+    def delete(self):
+        gl.glDeleteBuffer(self._handle)
+    
+    def activate(self):
+        gl.glBindBuffer(self._target, self._handle)
+    
+    def deactivate(self):
+        gl.glBindBuffer(self._target, 0)
+    
+    def set_size(self, nbytes):  # in bytes
+        if nbytes != self._buffer_size:
+            self.activate()
+            gl.glBufferData(self._target, nbytes, self._usage)
+            self._buffer_size = nbytes
+    
+    def set_data(self, offset, data):
+        self.activate()
+        nbytes = data.nbytes
+        
+        # Determine whether to check errors to try handling the ATI bug
+        check_ati_bug = ((not self._bufferSubDataOk) and
+                         (gl.current_backend is gl.gl2) and
+                         sys.platform.startswith('win'))
+
+        # flush any pending errors
+        if check_ati_bug:
+            gl.check_error('periodic check')
+        
+        try:
+            gl.glBufferSubData(self._target, offset, data)
+            if check_ati_bug:
+                gl.check_error('glBufferSubData')
+            self._bufferSubDataOk = True  # glBufferSubData seems to work
+        except Exception:
+            # This might be due to a driver error (seen on ATI), issue #64.
+            # We try to detect this, and if we can use glBufferData instead
+            if offset == 0 and nbytes == self._buffer_size:
+                gl.glBufferData(self._target, data, self._usage)
+                logger.debug("Using glBufferData instead of " +
+                             "glBufferSubData (known ATI bug).")
+            else:
+                raise    
+  
+
+class GlirVertexBuffer(GlirBuffer):
+    _target = gl.GL_ARRAY_BUFFER
+    
+
+class GlirIndexBuffer(GlirBuffer):
+    _target = gl.GL_ELEMENT_ARRAY_BUFFER
+
+
+class GlirTexture(GlirObject):
+    _target = None
+    
+    _types = {
+        np.dtype(np.int8): gl.GL_BYTE,
+        np.dtype(np.uint8): gl.GL_UNSIGNED_BYTE,
+        np.dtype(np.int16): gl.GL_SHORT,
+        np.dtype(np.uint16): gl.GL_UNSIGNED_SHORT,
+        np.dtype(np.int32): gl.GL_INT,
+        np.dtype(np.uint32): gl.GL_UNSIGNED_INT,
+        # np.dtype(np.float16) : gl.GL_HALF_FLOAT,
+        np.dtype(np.float32): gl.GL_FLOAT,
+        # np.dtype(np.float64) : gl.GL_DOUBLE
+    }
+    
+    def create(self):
+        self._handle = gl.glCreateTexture()
+        self._shape_formats = 0  # To make setting size cheap
+    
+    def delete(self):
+        gl.glDeleteTexture(self._handle)
+    
+    def activate(self):
+        gl.glBindTexture(self._target, self._handle)
+    
+    def deactivate(self):
+        gl.glBindTexture(self._target, 0)
+    
+    # Taken from pygly
+    def _get_alignment(self, width):
+        """Determines a textures byte alignment.
+
+        If the width isn't a power of 2
+        we need to adjust the byte alignment of the image.
+        The image height is unimportant
+
+        www.opengl.org/wiki/Common_Mistakes#Texture_upload_and_pixel_reads
+        """
+        # we know the alignment is appropriate
+        # if we can divide the width by the
+        # alignment cleanly
+        # valid alignments are 1,2,4 and 8
+        # put 4 first, since it's the default
+        alignments = [4, 8, 2, 1]
+        for alignment in alignments:
+            if width % alignment == 0:
+                return alignment
+    
+    def set_wrapping(self, wrapping):
+        self.activate()
+        wrapping = [as_enum(w) for w in wrapping]
+        if len(wrapping) == 3:
+            GL_TEXTURE_WRAP_R = 32882
+            gl.glTexParameterf(self._target, GL_TEXTURE_WRAP_R, wrapping[0])
+        if len(wrapping) >= 2:
+            gl.glTexParameterf(self._target, 
+                               gl.GL_TEXTURE_WRAP_S, wrapping[-2])
+        gl.glTexParameterf(self._target, gl.GL_TEXTURE_WRAP_T, wrapping[-1])
+
+    def set_interpolation(self, min, mag):
+        self.activate()
+        min, mag = as_enum(min), as_enum(mag)
+        gl.glTexParameterf(self._target, gl.GL_TEXTURE_MIN_FILTER, min)
+        gl.glTexParameterf(self._target, gl.GL_TEXTURE_MAG_FILTER, mag)
+
+# these should be auto generated in _constants.py. But that doesn't seem 
+# to be happening. TODO - figure out why the C parser in (createglapi.py)
+# is not extracting these constanst out.
+# found the constant value at:
+# http://docs.factorcode.org/content/word-GL_TEXTURE_1D,opengl.gl.html
+# http://docs.factorcode.org/content/word-GL_SAMPLER_1D%2Copengl.gl.html
+GL_SAMPLER_1D = gl.Enum('GL_SAMPLER_1D', 35677)
+GL_TEXTURE_1D = gl.Enum('GL_TEXTURE_1D', 3552)
+
+
+class GlirTexture1D(GlirTexture):
+    _target = GL_TEXTURE_1D
+    
+    def set_size(self, shape, format, internalformat):
+        format = as_enum(format)
+        if internalformat is not None:
+            internalformat = as_enum(internalformat)
+        else:
+            internalformat = format
+        # Shape is width
+        if (shape, format, internalformat) != self._shape_formats:
+            self.activate()
+            self._shape_formats = shape, format, internalformat
+            glTexImage1D(self._target, 0, internalformat, format,
+                         gl.GL_BYTE, shape[:1])
+    
+    def set_data(self, offset, data):
+        self.activate()
+        shape, format, internalformat = self._shape_formats
+        x = offset[0]
+        # Get gtype
+        gtype = self._types.get(np.dtype(data.dtype), None)
+        if gtype is None:
+            raise ValueError("Type %r not allowed for texture" % data.dtype)
+        # Set alignment (width is nbytes_per_pixel * npixels_per_line)
+        alignment = self._get_alignment(data.shape[-1])
+        if alignment != 4:
+            gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, alignment)
+        # Upload
+        glTexSubImage1D(self._target, 0, x, format, gtype, data)
+        # Set alignment back
+        if alignment != 4:
+            gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 4)
+
+
+class GlirTexture2D(GlirTexture):
+    _target = gl.GL_TEXTURE_2D
+    
+    def set_size(self, shape, format, internalformat):
+        # Shape is height, width
+        format = as_enum(format)
+        internalformat = format if internalformat is None \
+            else as_enum(internalformat)
+        if (shape, format, internalformat) != self._shape_formats:
+            self._shape_formats = shape, format, internalformat
+            self.activate()
+            gl.glTexImage2D(self._target, 0, internalformat, format,
+                            gl.GL_UNSIGNED_BYTE, shape[:2])
+    
+    def set_data(self, offset, data):
+        self.activate()
+        shape, format, internalformat = self._shape_formats
+        y, x = offset
+        # Get gtype
+        gtype = self._types.get(np.dtype(data.dtype), None)
+        if gtype is None:
+            raise ValueError("Type %r not allowed for texture" % data.dtype)
+        # Set alignment (width is nbytes_per_pixel * npixels_per_line)
+        alignment = self._get_alignment(data.shape[-2]*data.shape[-1])
+        if alignment != 4:
+            gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, alignment)
+        # Upload
+        gl.glTexSubImage2D(self._target, 0, x, y, format, gtype, data)
+        # Set alignment back
+        if alignment != 4:
+            gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 4)
+
+
+GL_SAMPLER_3D = gl.Enum('GL_SAMPLER_3D', 35679)
+GL_TEXTURE_3D = gl.Enum('GL_TEXTURE_3D', 32879)
+
+USE_TEX_3D = False
+
+
+def _check_pyopengl_3D():
+    """Helper to ensure users have OpenGL for 3D texture support (for now)"""
+    global USE_TEX_3D
+    USE_TEX_3D = True
+    try:
+        import OpenGL.GL as _gl
+    except ImportError:
+        raise ImportError('PyOpenGL is required for 3D texture support')
+    return _gl
+
+
+def glTexImage3D(target, level, internalformat, format, type, pixels):
+    # Import from PyOpenGL
+    _gl = _check_pyopengl_3D()
+    border = 0
+    assert isinstance(pixels, (tuple, list))  # the only way we use this now
+    depth, height, width = pixels
+    _gl.glTexImage3D(target, level, internalformat,
+                     width, height, depth, border, format, type, None)
+
+
+def glTexImage1D(target, level, internalformat, format, type, pixels):
+    # Import from PyOpenGL
+    _gl = _check_pyopengl_3D()
+    border = 0
+    assert isinstance(pixels, (tuple, list))  # the only way we use this now
+    # pixels will be a tuple of the form (width, )
+    # we only need the first argument
+    width = pixels[0]
+
+    _gl.glTexImage1D(target, level, internalformat,
+                     width, border, format, type, None)
+
+
+def glTexSubImage1D(target, level, xoffset,
+                    format, type, pixels):
+    # Import from PyOpenGL
+    _gl = _check_pyopengl_3D()
+    width = pixels.shape[:1]
+
+    # width will be a tuple of the form (w, )
+    # we need to take the first element (integer)
+    _gl.glTexSubImage1D(target, level, xoffset,
+                        width[0], format, type, pixels)
+
+
+def glTexSubImage3D(target, level, xoffset, yoffset, zoffset,
+                    format, type, pixels):
+    # Import from PyOpenGL
+    _gl = _check_pyopengl_3D()
+    depth, height, width = pixels.shape[:3]
+    _gl.glTexSubImage3D(target, level, xoffset, yoffset, zoffset,
+                        width, height, depth, format, type, pixels)
+
+
+class GlirTexture3D(GlirTexture):
+    _target = GL_TEXTURE_3D
+
+    def set_size(self, shape, format, internalformat):
+        format = as_enum(format)
+        if internalformat is not None:
+            internalformat = as_enum(internalformat)
+        else:
+            internalformat = format
+        # Shape is depth, height, width
+        if (shape, format, internalformat) != self._shape_formats:
+            self.activate()
+            self._shape_formats = shape, format, internalformat
+            glTexImage3D(self._target, 0, internalformat, format,
+                         gl.GL_BYTE, shape[:3])
+    
+    def set_data(self, offset, data):
+        self.activate()
+        shape, format, internalformat = self._shape_formats
+        z, y, x = offset
+        # Get gtype
+        gtype = self._types.get(np.dtype(data.dtype), None)
+        if gtype is None:
+            raise ValueError("Type not allowed for texture")
+        # Set alignment (width is nbytes_per_pixel * npixels_per_line)
+        alignment = self._get_alignment(data.shape[-3] *
+                                        data.shape[-2] * data.shape[-1])
+        if alignment != 4:
+            gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, alignment)
+        # Upload
+        glTexSubImage3D(self._target, 0, x, y, z, format, gtype, data)
+        # Set alignment back
+        if alignment != 4:
+            gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 4)
+
+
+class GlirRenderBuffer(GlirObject):
+    
+    def create(self):
+        self._handle = gl.glCreateRenderbuffer()
+        self._shape_format = 0  # To make setting size cheap
+    
+    def delete(self):
+        gl.glDeleteRenderbuffer(self._handle)
+    
+    def activate(self):
+        gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._handle)
+    
+    def deactivate(self):
+        gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, 0)
+    
+    def set_size(self, shape, format):
+        if isinstance(format, string_types):
+            format = GlirFrameBuffer._formats[format][1]
+        if (shape, format) != self._shape_format:
+            self._shape_format = shape, format
+            self.activate()
+            gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, format,
+                                     shape[1], shape[0])
+
+
+class GlirFrameBuffer(GlirObject):
+    
+    # todo: on ES 2.0 -> gl.gl_RGBA4
+    _formats = {'color': (gl.GL_COLOR_ATTACHMENT0, gl.GL_RGBA),
+                'depth': (gl.GL_DEPTH_ATTACHMENT, gl.GL_DEPTH_COMPONENT16),
+                'stencil': (gl.GL_STENCIL_ATTACHMENT, gl.GL_STENCIL_INDEX8)}
+    
+    def create(self):
+        #self._parser._fb_stack = [0]  # To keep track of active FB
+        self._handle = gl.glCreateFramebuffer()
+        self._validated = False
+    
+    def delete(self):
+        gl.glDeleteFramebuffer(self._handle)
+
+    def set_framebuffer(self, yes):
+        if yes:
+            if not self._validated:
+                self._validated = True
+                self._validate()
+            self.activate()
+        else:
+            self.deactivate()
+    
+    def activate(self):
+        stack = self._parser.env.setdefault('fb_stack', [0])
+        if stack[-1] != self._handle:
+            stack.append(self._handle)
+            gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._handle)
+    
+    def deactivate(self):
+        stack = self._parser.env.setdefault('fb_stack', [0])
+        while self._handle in stack:
+            stack.remove(self._handle)
+        gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, stack[-1])
+    
+    def attach(self, attachment, buffer_id):
+        attachment = GlirFrameBuffer._formats[attachment][0]
+        self.activate()
+        if buffer_id == 0:
+            gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, attachment,
+                                         gl.GL_RENDERBUFFER, 0)
+        else:
+            buffer = self._parser.get_object(buffer_id)
+            if buffer == JUST_DELETED:
+                return
+            if buffer is None:
+                raise ValueError("Unknown buffer with id %i for attachement" % 
+                                 buffer_id)
+            elif isinstance(buffer, GlirRenderBuffer):
+                buffer.activate()
+                gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, attachment,
+                                             gl.GL_RENDERBUFFER, buffer.handle)
+                buffer.deactivate()
+            elif isinstance(buffer, GlirTexture2D):
+                buffer.activate()
+                # INFO: 0 is for mipmap level 0 (default) of the texture
+                gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, attachment,
+                                          gl.GL_TEXTURE_2D, buffer.handle, 0)
+                buffer.deactivate()
+            else:
+                raise ValueError("Invalid attachment: %s" % type(buffer))
+        self._validated = False
+        self.deactivate()
+    
+    def _validate(self):
+        res = gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER)
+        if res == gl.GL_FRAMEBUFFER_COMPLETE:
+            pass
+        elif res == 0:
+            raise RuntimeError('Target not equal to GL_FRAMEBUFFER')
+        elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
+            raise RuntimeError(
+                'FrameBuffer attachments are incomplete.')
+        elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
+            raise RuntimeError(
+                'No valid attachments in the FrameBuffer.')
+        elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
+            raise RuntimeError(
+                'attachments do not have the same width and height.')
+        #elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_FORMATS: # not in es 2.0
+        #    raise RuntimeError('Internal format of attachment '
+        #                       'is not renderable.')
+        elif res == gl.GL_FRAMEBUFFER_UNSUPPORTED:
+            raise RuntimeError('Combination of internal formats used '
+                               'by attachments is not supported.')
+        else:
+            raise RuntimeError('Unknown framebuffer error: %r.' % res)
diff --git a/vispy/gloo/globject.py b/vispy/gloo/globject.py
index d0962a6..ebb56ee 100644
--- a/vispy/gloo/globject.py
+++ b/vispy/gloo/globject.py
@@ -1,89 +1,108 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
+"""
+Base gloo object
+
+On queues
+---------
+
+The queue on the GLObject can be associated with other queues. These
+can be queues of other gloo objects, or of the canvas.context. A program
+associates the textures/buffers when they are set via __setitem__. A
+FrameBuffer does so when assigning buffers. A program associates itself
+with the canvas.context in draw(). A FrameBuffer does the same in
+activate().
+
+Example:
+
+    prog1, prog2 = Program(), Program()
+    tex1, tex2 = Texture(), Texture()
+
+    prog1.glir.associate(tex1.glir)
+    prog1.glir.associate(tex2.glir)
+
+    canvas1.context.glir.associate(prog1.glir)
+    canvas1.context.glir.associate(prog2.glir)
+    canvas2.context.glir.associate(prog2.glir)
+
+Now, when canvas1 flushes its queue, it takes all the pending commands
+from prog1 and prog2, and subsequently from tex1 and tex2. When canvas2
+is flushed, only commands from prog2 get taken. A similar situation
+holds for a texture that is associated with a program and a frame
+buffer.
+"""
+
+from .glir import GlirQueue
+
 
 class GLObject(object):
-    """ Generic GL object that may live both on CPU and GPU 
+    """ Generic GL object that represents an object on the GPU.
+    
+    When a GLObject is instantiated, it is associated with the currently
+    active Canvas, or with the next Canvas to be created if there is
+    no current Canvas
     """
-
+    
+    # Type of GLIR object, reset in subclasses
+    _GLIR_TYPE = 'DummyGlirType'
+    
     # Internal id counter to keep track of GPU objects
     _idcount = 0
-
+    
     def __init__(self):
         """ Initialize the object in the default state """
-
-        self._handle = -1
-        self._target = None
-        self._need_create = True
-        self._need_delete = False
-
+        
+        # Give this object an id
         GLObject._idcount += 1
         self._id = GLObject._idcount
-
+        
+        # Create the GLIR queue in which we queue our commands. 
+        # See docs above for details.
+        self._glir = GlirQueue()
+        
+        # Give glir command to create GL representation of this object
+        self._glir.command('CREATE', self._id, self._GLIR_TYPE)
+    
     def __del__(self):
         # You never know when this is goint to happen. The window might
         # already be closed and no OpenGL context might be available.
-        # Worse, there might be multiple contexts and calling delete()
-        # at the wrong moment might remove other gl objects, leading to
-        # very strange and hard to debug behavior.
-        #
-        # So we don't do anything. If each GLObject was aware of the
-        # context in which it resides, we could do auto-cleanup though...
-        # todo: it's not very Pythonic to have to delete an object.
-        pass
+        # However, since we are using GLIR queue, this does not matter!
+        # If the command gets transported to the canvas, that is great,
+        # if not, this probably means that the canvas no longer exists.
+        self.delete()
 
     def delete(self):
-        """ Delete the object from GPU memory """
-
-        if self._need_delete:
-            self._delete()
-        self._handle = -1
-        self._need_create = True
-        self._need_delete = False
-
-    def activate(self):
-        """ Activate the object on GPU """
-        # As a base class, we only provide functionality for
-        # automatically creating the object. The other stages are so
-        # different that it's more clear if each GLObject specifies
-        # what it does in _activate().
-        if self._need_create:
-            self._create()
-            self._need_create = False
-        self._activate()
-
-    def deactivate(self):
-        """ Deactivate the object on GPU """
-
-        self._deactivate()
+        """ Delete the object from GPU memory. 
+
+        Note that the GPU object will also be deleted when this gloo
+        object is about to be deleted. However, sometimes you want to
+        explicitly delete the GPU object explicitly.
+        """
+        # We only allow the object from being deleted once, otherwise
+        # we might be deleting another GPU object that got our gl-id
+        # after our GPU object was deleted. Also note that e.g.
+        # DataBufferView does not have the _glir attribute.
+        if hasattr(self, '_glir'):
+            # Send our final command into the queue
+            self._glir.command('DELETE', self._id)
+            # Tell master glir queue that this queue is no longer being used
+            self._glir._deletable = True
+            # Detach the queue
+            del self._glir
 
     @property
-    def handle(self):
-        """ Name of this object on the GPU """
-
-        return self._handle
-
+    def id(self):
+        """ The id of this GL object used to reference the GL object
+        in GLIR. id's are unique within a process.
+        """
+        return self._id
+    
     @property
-    def target(self):
-        """ OpenGL type of object. """
-
-        return self._target
-
-    def _create(self):
-        """ Dummy create method """
-        raise NotImplementedError()
-
-    def _delete(self):
-        """ Dummy delete method """
-        raise NotImplementedError()
-
-    def _activate(self):
-        """ Dummy activate method """
-        raise NotImplementedError()
-
-    def _deactivate(self):
-        """ Dummy deactivate method """
-        raise NotImplementedError()
+    def glir(self):
+        """ The glir queue for this object.
+        """
+        return self._glir
diff --git a/vispy/gloo/initialize.py b/vispy/gloo/initialize.py
deleted file mode 100644
index 13483ae..0000000
--- a/vispy/gloo/initialize.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from . import gl
-
-GL_VERTEX_PROGRAM_POINT_SIZE = 34370
-GL_POINT_SPRITE = 34913
-
-
-def gl_initialize():
-    """Initialize GL values
-
-    This method helps standardize GL across desktop and mobile, e.g.
-    by enabling ``GL_VERTEX_PROGRAM_POINT_SIZE`` and ``GL_POINT_SPRITE``.
-    """
-    gl.glEnable(GL_VERTEX_PROGRAM_POINT_SIZE)
-    gl.glEnable(GL_POINT_SPRITE)
diff --git a/vispy/gloo/preprocessor.py b/vispy/gloo/preprocessor.py
new file mode 100644
index 0000000..5365015
--- /dev/null
+++ b/vispy/gloo/preprocessor.py
@@ -0,0 +1,70 @@
+
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+import re
+from .. import glsl
+from ..util import logger
+
+
+def remove_comments(code):
+    """Remove C-style comment from GLSL code string."""
+
+    pattern = r"(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*\n)"
+    # first group captures quoted strings (double or single)
+    # second group captures comments (//single-line or /* multi-line */)
+    regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
+
+    def do_replace(match):
+        # if the 2nd group (capturing comments) is not None,
+        # it means we have captured a non-quoted (real) comment string.
+        if match.group(2) is not None:
+            return ""  # so we will return empty to remove the comment
+        else:  # otherwise, we will return the 1st group
+            return match.group(1)  # captured quoted-string
+
+    return regex.sub(do_replace, code)
+
+
+def merge_includes(code):
+    """Merge all includes recursively."""
+
+    pattern = '\#\s*include\s*"(?P<filename>[a-zA-Z0-9\_\-\.\/]+)"'
+    regex = re.compile(pattern)
+    includes = []
+
+    def replace(match):
+        filename = match.group("filename")
+
+        if filename not in includes:
+            includes.append(filename)
+            path = glsl.find(filename)
+            if not path:
+                logger.critical('"%s" not found' % filename)
+                raise RuntimeError("File not found", filename)
+            text = '\n// --- start of "%s" ---\n' % filename
+            with open(path) as fh:
+                text += fh.read()
+            text += '// --- end of "%s" ---\n' % filename
+            return text
+        return ''
+
+    # Limit recursion to depth 10
+    for i in range(10):
+        if re.search(regex, code):
+            code = re.sub(regex, replace, code)
+        else:
+            break
+
+    return code
+
+
+def preprocess(code):
+    """Preprocess a code by removing comments, version and merging includes."""
+
+    if code:
+        #code = remove_comments(code)
+        code = merge_includes(code)
+    return code
diff --git a/vispy/gloo/program.py b/vispy/gloo/program.py
index cda1e48..f594390 100644
--- a/vispy/gloo/program.py
+++ b/vispy/gloo/program.py
@@ -1,27 +1,43 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
+
+
+"""
+Implementation of a GL Program object. 
+
+This class parses the source code to obtain the names and types of
+uniforms, attributes, varyings and constants. This information is used
+to provide the user with a natural way to set variables.
+
+Gloo vs GLIR
+------------
+
+Done in this class:
+  * Check the data shape given for uniforms and attributes
+  * Convert uniform data to array of the correct type
+  * Check whether any variables are set that are not present in source code
+
+Done by GLIR:
+  * Check whether a set uniform/attribute is not active (a warning is given)
+  * Check whether anactive attribute or uniform is not set (a warning is given)
+
+
+"""
+
 import re
 import numpy as np
 
-from . import gl
 from .globject import GLObject
-from .buffer import VertexBuffer, IndexBuffer
-from .shader import VertexShader, FragmentShader
-from .texture import GL_SAMPLER_3D
-from .variable import Uniform, Attribute
-from .wrappers import _check_conversion
+from .buffer import VertexBuffer, IndexBuffer, DataBuffer
+from .texture import BaseTexture, Texture2D, Texture3D, Texture1D
 from ..util import logger
-
-
-_known_draw_modes = dict()
-for key in ('points', 'lines', 'line_strip', 'line_loop',
-            'triangles', 'triangle_strip', 'triangle_fan'):
-    x = getattr(gl, 'GL_' + key.upper())
-    _known_draw_modes[key] = x
-    _known_draw_modes[x] = x  # for speed in this case
+from .util import check_enum 
+from ..ext.six import string_types
+from .context import get_current_canvas
+from .preprocessor import preprocess
 
 
 # ----------------------------------------------------------- Program class ---
@@ -30,302 +46,181 @@ class Program(GLObject):
 
     A Program is an object to which shaders can be attached and linked to
     create the final program.
-
+    
+    Uniforms and attributes can be set using indexing: e.g.
+    ``program['a_pos'] = pos_data`` and ``program['u_color'] = (1, 0, 0)``.
+    
     Parameters
     ----------
-    vert : str, VertexShader, or list
+    vert : str
         The vertex shader to be used by this program
-    frag : str, FragmentShader, or list
+    frag : str
         The fragment shader to be used by this program
     count : int (optional)
-        Number of vertices this program will use. This can be given to
-        initialize a VertexBuffer during Program initialization.
-
+        The program will prepare a structured vertex buffer of count
+        vertices. All attributes set using ``prog['attr'] = X`` will
+        be combined into a structured vbo with interleaved elements, which
+        is more efficient than having one vbo per attribute.
+    
     Notes
     -----
     If several shaders are specified, only one can contain the main
     function. OpenGL ES 2.0 does not support a list of shaders.
     """
-
+    
+    _GLIR_TYPE = 'Program'
+    
+    _gtypes = {  # DTYPE, NUMEL
+        'float':        (np.float32, 1),
+        'vec2':         (np.float32, 2),
+        'vec3':         (np.float32, 3),
+        'vec4':         (np.float32, 4),
+        'int':          (np.int32,   1),
+        'ivec2':        (np.int32,   2),
+        'ivec3':        (np.int32,   3),
+        'ivec4':        (np.int32,   4),
+        'bool':         (np.bool,    1),
+        'bvec2':        (np.bool,    2),
+        'bvec3':        (np.bool,    3),
+        'bvec4':        (np.bool,    4),
+        'mat2':         (np.float32, 4),
+        'mat3':         (np.float32, 9),
+        'mat4':         (np.float32, 16),
+        'sampler1D':    (np.uint32, 1),
+        'sampler2D':    (np.uint32, 1),
+        'sampler3D':    (np.uint32, 1),
+    }
+    
     # ---------------------------------
     def __init__(self, vert=None, frag=None, count=0):
         GLObject.__init__(self)
-
-        self._count = count
-        self._buffer = None
         
-        self._need_build = True
+        # Init source code for vertex and fragment shader
+        self._shaders = '', '' 
         
-        # Init uniforms and attributes
-        self._uniforms = {}
-        self._attributes = {}
+        # Init description of variables obtained from source code
+        self._code_variables = {}  # name -> (kind, type_, name)
+        # Init user-defined data for attributes and uniforms
+        self._user_variables = {}  # name -> data / buffer / texture
+        # Init pending user-defined data
+        self._pending_variables = {}  # name -> data
         
-        # Get all vertex shaders
-        self._verts = []
-        if isinstance(vert, (str, VertexShader)):
-            verts = [vert]
-        elif isinstance(vert, (type(None), tuple, list)):
-            verts = vert or []
-        else:
-            raise ValueError('Vert must be str, VertexShader or list')
-        # Apply
-        for shader in verts:
-            if isinstance(shader, str):
-                self._verts.append(VertexShader(shader))
-            elif isinstance(shader, VertexShader):
-                if shader not in self._verts:
-                    self._verts.append(shader)
-            else:
-                T = type(shader)
-                raise ValueError('Cannot make a VertexShader of %r.' % T)
-
-        # Get all fragment shaders
-        self._frags = []
-        if isinstance(frag, (str, FragmentShader)):
-            frags = [frag]
-        elif isinstance(frag, (type(None), tuple, list)):
-            frags = frag or []
-        else:
-            raise ValueError('Frag must be str, FragmentShader or list')
-        # Apply
-        for shader in frags:
-            if isinstance(shader, str):
-                self._frags.append(FragmentShader(shader))
-            elif isinstance(shader, FragmentShader):
-                if shader not in self._frags:
-                    self._frags.append(shader)
-            else:
-                T = type(shader)
-                raise ValueError('Cannot make a FragmentShader of %r.' % T)
-
-        # Build uniforms and attributes
-        self._create_variables()
-
-        # Build associated structured vertex buffer if count is given
+        # NOTE: we *could* allow vert and frag to be a tuple/list of shaders,
+        # but that would complicate the GLIR implementation, and it seems 
+        # unncessary
+        
+        # Check and set shaders
+        if isinstance(vert, string_types) and isinstance(frag, string_types):
+            self.set_shaders(vert, frag)
+        elif not (vert is None and frag is None):
+            raise ValueError('Vert and frag must either both be str or None')
+        
+        # Build associated structured vertex buffer if count is given.
+        # This makes it easy to create a structured vertex buffer
+        # without having to create a numpy array with structured dtype.
+        # All assignments must be done before the GLIR commands are
+        # sent away for parsing (in draw) though.
+        self._count = count
+        self._buffer = None  # Set to None in draw()
         if self._count > 0:
             dtype = []
-            for attribute in self._attributes.values():
-                dtype.append(attribute.dtype)
-            self._buffer = VertexBuffer(np.zeros(self._count, dtype=dtype))
-            self.bind(self._buffer)
-
-    def attach(self, shaders):
-        """ Attach one or several vertex/fragment shaders to the program
-        
-        Note that GL ES 2.0 only supports one vertex and one fragment 
-        shader.
+            for kind, type_, name, size in self._code_variables.values():
+                if kind == 'attribute':
+                    dt, numel = self._gtypes[type_]
+                    dtype.append((name, dt, numel))
+            self._buffer = np.zeros(self._count, dtype=dtype)
+            self.bind(VertexBuffer(self._buffer))
+
+    def set_shaders(self, vert, frag):
+        """ Set the vertex and fragment shaders.
         
         Parameters
         ----------
-        
-        shaders : list of shade objects
-            The shaders to attach.
+        vert : str
+            Source code for vertex shader.
+        frag : str
+            Source code for fragment shaders.
         """
-
-        if isinstance(shaders, (VertexShader, FragmentShader)):
-            shaders = [shaders]
-        for shader in shaders:
-            if isinstance(shader, VertexShader):
-                self._verts.append(shader)
-            else:
-                self._frags.append(shader)
-
-        # Ensure uniqueness of shaders
-        self._verts = list(set(self._verts))
-        self._frags = list(set(self._frags))
-
-        self._need_create = True
-        self._need_build = True
-
-        # Build uniforms and attributes
-        self._create_variables()
-
-    def detach(self, shaders):
-        """Detach one or several vertex/fragment shaders from the program.
-    
-        Parameters
-        ----------
-        shaders : list of shade objects
-            The shaders to detach.
+        if not vert or not frag:
+            raise ValueError('Vertex and fragment code must both be non-empty')
         
-        Notes
-        -----
-        We don't need to defer attach/detach shaders since shader deletion
-        takes care of that.
-        """
-
-        if isinstance(shaders, (VertexShader, FragmentShader)):
-            shaders = [shaders]
-        for shader in shaders:
-            if isinstance(shader, VertexShader):
-                if shader in self._verts:
-                    self._verts.remove(shader)
-                else:
-                    raise RuntimeError("Shader is not attached to the program")
-            if isinstance(shader, FragmentShader):
-                if shader in self._frags:
-                    self._frags.remove(shader)
-                else:
-                    raise RuntimeError("Shader is not attached to the program")
-        self._need_build = True
-
-        # Build uniforms and attributes
-        self._create_variables()
-
-    def _create(self):
-        """
-        Create the GL program object if needed.
-        """
-        # Check if program has been created
-        if self._handle <= 0:
-            self._handle = gl.glCreateProgram()
-            if not self._handle:
-                raise RuntimeError("Cannot create program object")
+        # pre-process shader code for #include directives
+        vert, frag = preprocess(vert), preprocess(frag)
+        
+        # Store source code, send it to glir, parse the code for variables
+        self._shaders = vert, frag
+
+        self._glir.command('SHADERS', self._id, vert, frag)
+        # All current variables become pending variables again
+        for key, val in self._user_variables.items():
+            self._pending_variables[key] = val
+        self._user_variables = {}
+        # Parse code (and process pending variables)
+        self._parse_variables_from_code()
     
-    def _delete(self):
-        logger.debug("GPU: Deleting program")
-        gl.glDeleteProgram(self._handle)
+    @property
+    def shaders(self):
+        """ Source code for vertex and fragment shader
+        """
+        return self._shaders
     
-    def _activate(self):
-        """Activate the program as part of current rendering state."""
-        
-        #logger.debug("GPU: Activating program")
-        
-        # Check whether we need to rebuild shaders and create variables
-        if any(s._need_compile for s in self.shaders):
-            self._need_build = True
-        
-        # Stuff we need to do *before* glUse-ing the program
-        did_build = False
-        if self._need_build:
-            did_build = True
-            self._build()
-            self._need_build = False
-        
-        # Go and use the prrogram
-        gl.glUseProgram(self.handle)
+    @property
+    def variables(self):
+        """ A list of the variables in use by the current program
         
-        # Stuff we need to do *after* glUse-ing the program
-        self._activate_variables()
+        The list is obtained by parsing the GLSL source code. 
         
-        # Validate. We need to validate after textures units get assigned
-        # (glUniform1i() gets called in _update() in variable.py)
-        if did_build:
-            gl.glValidateProgram(self._handle)
-            if not gl.glGetProgramParameter(self._handle, 
-                                            gl.GL_VALIDATE_STATUS):
-                print(gl.glGetProgramInfoLog(self._handle))
-                raise RuntimeError('Program validation error')
-    
-    def _deactivate(self):
-        """Deactivate the program."""
-
-        logger.debug("GPU: Deactivating program")
-        gl.glUseProgram(0)
-        self._deactivate_variables()
-    
-    def _build(self):
+        Returns
+        -------
+        variables : list
+            Each variable is represented as a tuple (kind, type, name),
+            where `kind` is 'attribute', 'uniform', 'uniform_array',
+            'varying' or 'const'.
         """
-        Build (link) the program and checks everything's ok.
-
-        A GL context must be available to be able to build (link)
+        # Note that internally the variables are stored as a dict
+        # that maps names -> tuples, for easy looking up by name.
+        return [x[:3] for x in self._code_variables.values()]
+   
+    def _parse_variables_from_code(self):
+        """ Parse uniforms, attributes and varyings from the source code.
         """
-        # Check if we have something to link
-        if not self._verts:
-            raise ValueError("No vertex shader has been given")
-        if not self._frags:
-            raise ValueError("No fragment shader has been given")
-
-        # Detach any attached shaders
-        attached = gl.glGetAttachedShaders(self._handle)
-        for handle in attached:
-            gl.glDetachShader(self._handle, handle)
-
-        # Attach vertex and fragment shaders
-        for shader in self._verts:
-            shader.activate()
-            gl.glAttachShader(self._handle, shader.handle)
-        for shader in self._frags:
-            shader.activate()
-            gl.glAttachShader(self._handle, shader.handle)
-
-        logger.debug("GPU: Creating program")
-
-        # Link the program
-        gl.glLinkProgram(self._handle)
-        if not gl.glGetProgramParameter(self._handle, gl.GL_LINK_STATUS):
-            print(gl.glGetProgramInfoLog(self._handle))
-            raise RuntimeError('Program linking error')
         
-        # Now we know what variable will be used by the program
-        self._enable_variables()
-    
-    def _create_variables(self):
-        """ Create the uniform and attribute objects based on the
-        provided GLSL. This method is called when the GLSL is changed.
-        """
+        # Get one string of code with comments removed
+        code = '\n\n'.join(self._shaders)
+        code = re.sub(r'(.*)(//.*)', r'\1', code, re.M)
         
-        # todo: maybe we want to restore previously set variables, 
-        # so that uniforms and attributes do not have to be set each time
-        # that the shaders are updated. However, we should take into account
-        # that typically all shaders are removed (i.e. no variables are
-        # present) and then the new shaders are added.
-
-        # Build uniforms
-        self._uniforms = {}
-        count = 0
-        for (name, gtype) in self.all_uniforms:
-            uniform = Uniform(self, name, gtype)
-            # if gtype in (gl.GL_SAMPLER_1D, gl.GL_SAMPLER_2D):
-            if gtype in (gl.GL_SAMPLER_2D, GL_SAMPLER_3D):
-                uniform._unit = count
-                count += 1
-            self._uniforms[name] = uniform
+        # Regexp to look for variable names
+        var_regexp = ("\s*VARIABLE\s+"  # kind of variable
+                      "((highp|mediump|lowp)\s+)?"  # Precision (optional)
+                      "(?P<type>\w+)\s+"  # type
+                      "(?P<name>\w+)\s*"  # name
+                      "(\[(?P<size>\d+)\])?"  # size (optional)
+                      "(\s*\=\s*[0-9.]+)?"  # default value (optional)
+                      "\s*;"  # end
+                      )
         
-        # Build attributes
-        self._attributes = {}
-        dtype = []
-        for (name, gtype) in self.all_attributes:
-            attribute = Attribute(self, name, gtype)
-            self._attributes[name] = attribute
-            dtype.append(attribute.dtype)
-    
-    def _enable_variables(self):  # previously _update
-        """ Enable the uniform and attribute objects that will actually be
-        used by the Program. i.e. variables that are optimised out are
-        disabled. This method is called after the program has been buid.
-        """
-        # Enable uniforms
-        active_uniforms = [name for (name, gtype) in self.active_uniforms]
-        for uniform in self._uniforms.values():
-            uniform.enabled = uniform.name in active_uniforms
-        # Enable attributes
-        active_attributes = [name for (name, gtype) in self.active_attributes]
-        for attribute in self._attributes.values():
-            attribute.enabled = attribute.name in active_attributes
-    
-    def _activate_variables(self):
-        """ Activate the uniforms and attributes so that the Program
-        can use them. This method is called when the Program gets activated.
-        """
-        for uniform in self._uniforms.values():
-            if uniform.enabled:
-                uniform.activate()
-        for attribute in self._attributes.values():
-            if attribute.enabled:
-                attribute.activate()
+        # Parse uniforms, attributes and varyings
+        self._code_variables = {}
+        for kind in ('uniform', 'attribute', 'varying', 'const'):
+            regex = re.compile(var_regexp.replace('VARIABLE', kind),
+                               flags=re.MULTILINE)
+            for m in re.finditer(regex, code):
+                gtype = m.group('type')
+                size = int(m.group('size')) if m.group('size') else -1
+                this_kind = kind
+                if size >= 1:
+                    # uniform arrays get added both as individuals and full
+                    for i in range(size):
+                        name = '%s[%d]' % (m.group('name'), i)
+                        self._code_variables[name] = kind, gtype, name, -1
+                    this_kind = 'uniform_array'
+                name = m.group('name')
+                self._code_variables[name] = this_kind, gtype, name, size
+
+        # Now that our code variables are up-to date, we can process
+        # the variables that were set but yet unknown.
+        self._process_pending_variables()
 
-    def _deactivate_variables(self):
-        """ Deactivate all enabled uniforms and attributes. This method
-        gets called when the Program gets deactivated.
-        """
-        for uniform in self._uniforms.values():
-            if uniform.enabled:
-                uniform.deactivate()
-        for attribute in self._attributes.values():
-            if attribute.enabled:
-                attribute.deactivate()
-    
     def bind(self, data):
         """ Bind a VertexBuffer that has structured data
         
@@ -340,160 +235,202 @@ class Program(GLObject):
             raise ValueError('Program.bind() requires a VertexBuffer.')
         # Apply
         for name in data.dtype.names:
-            if name in self._attributes.keys():
-                self._attributes[name].set_data(data[name])
-            else:
-                logger.warning("%s has not been bound" % name)
-
+            self[name] = data[name]
+    
+    def _process_pending_variables(self):
+        """ Try to apply the variables that were set but not known yet.
+        """
+        # Clear our list of pending variables
+        self._pending_variables, pending = {}, self._pending_variables
+        # Try to apply it. On failure, it will be added again
+        for name, data in pending.items():
+            self[name] = data
+    
     def __setitem__(self, name, data):
-        try:
-            if name in self._uniforms.keys():
-                self._uniforms[name].set_data(data)
-            elif name in self._attributes.keys():
-                self._attributes[name].set_data(data)
+        """ Setting uniform or attribute data
+        
+        This method requires the information about the variable that we
+        know from parsing the source code. If this information is not
+        yet available, the data is stored in a list of pending data,
+        and we attempt to set it once new shading code has been set.
+        
+        For uniforms, the data can represent a plain uniform or a
+        sampler. In the latter case, this method accepts a Texture
+        object or a numpy array which is used to update the existing
+        texture. A new texture is created if necessary.
+
+        For attributes, the data can be a tuple/float which GLSL will
+        use for the value of all vertices. This method also acceps VBO
+        data as a VertexBuffer object or a numpy array which is used
+        to update the existing VertexBuffer. A new VertexBuffer is
+        created if necessary.
+        
+        By passing None as data, the uniform or attribute can be
+        "unregistered". This can be useful to get rid of variables that
+        are no longer present or active in the new source code that is
+        about to be set.
+        """
+        
+        # Deal with local buffer storage (see count argument in __init__)
+        if (self._buffer is not None) and not isinstance(data, DataBuffer):
+            if name in self._buffer.dtype.names:
+                self._buffer[name] = data
+                return
+        
+        # Delete?
+        if data is None:
+            self._user_variables.pop(name, None)
+            self._pending_variables.pop(name, None)
+            return
+        
+        if name in self._code_variables:
+            kind, type_, name, size = self._code_variables[name]
+            
+            if kind == 'uniform':
+                if type_.startswith('sampler'):
+                    # Texture data; overwrite or update
+                    tex = self._user_variables.get(name, None)
+                    if isinstance(data, BaseTexture):
+                        pass
+                    elif tex and hasattr(tex, 'set_data'):
+                        tex.set_data(data)
+                        return
+                    elif type_ == 'sampler1D':
+                        data = Texture1D(data)
+                    elif type_ == 'sampler2D':
+                        data = Texture2D(data)
+                    elif type_ == 'sampler3D':
+                        data = Texture3D(data)
+                    else:
+                        # This should not happen
+                        raise RuntimeError('Unknown type %s' % type_)
+                    # Store and send GLIR command
+                    self._user_variables[name] = data
+                    self.glir.associate(data.glir)
+                    self._glir.command('TEXTURE', self._id, name, data.id)
+                else:
+                    # Normal uniform; convert to np array and check size
+                    dtype, numel = self._gtypes[type_]
+                    data = np.array(data, dtype=dtype).ravel()
+                    if data.size != numel:
+                        raise ValueError('Uniform %r needs %i elements, '
+                                         'not %i.' % (name, numel, data.size))
+                    # Store and send GLIR command
+                    self._user_variables[name] = data
+                    self._glir.command('UNIFORM', self._id, name, type_, data)
+
+            elif kind == 'uniform_array':
+                # Normal uniform; convert to np array and check size
+                dtype, numel = self._gtypes[type_]
+                data = np.atleast_2d(data).astype(dtype)
+                need_shape = (size, numel)
+                if data.shape != need_shape:
+                    raise ValueError('Uniform array %r needs shape %s not %s'
+                                     % (name, need_shape, data.shape))
+                data = data.ravel()
+                # Store and send GLIR command
+                self._user_variables[name] = data
+                self._glir.command('UNIFORM', self._id, name, type_, data)
+            
+            elif kind == 'attribute':
+                # Is this a constant value per vertex
+                is_constant = False
+
+                def isscalar(x):
+                    return isinstance(x, (float, int))
+
+                if isscalar(data):
+                    is_constant = True
+                elif isinstance(data, (tuple, list)):
+                    is_constant = all([isscalar(e) for e in data])
+                
+                if not is_constant:
+                    # VBO data; overwrite or update
+                    vbo = self._user_variables.get(name, None)
+                    if isinstance(data, DataBuffer):
+                        pass
+                    elif vbo is not None and hasattr(vbo, 'set_data'):
+                        vbo.set_data(data)
+                        return
+                    else:
+                        data = VertexBuffer(data)
+                    # Store and send GLIR command
+                    if data.dtype is not None:
+                        numel = self._gtypes[type_][1]
+                        if data._last_dim and data._last_dim != numel:
+                            raise ValueError('data.shape[-1] must be %s '
+                                             'not %s for %s'
+                                             % (numel, data._last_dim, name))
+                    self._user_variables[name] = data
+                    value = (data.id, data.stride, data.offset)
+                    self.glir.associate(data.glir)
+                    self._glir.command('ATTRIBUTE', self._id,
+                                       name, type_, value)
+                else:
+                    # Single-value attribute; convert to array and check size
+                    dtype, numel = self._gtypes[type_]
+                    data = np.array(data, dtype=dtype)
+                    if data.ndim == 0:
+                        data.shape = data.size
+                    if data.size != numel:
+                        raise ValueError('Attribute %r needs %i elements, '
+                                         'not %i.' % (name, numel, data.size))
+                    # Store and send GLIR command
+                    self._user_variables[name] = data
+                    value = tuple([0] + [i for i in data])
+                    self._glir.command('ATTRIBUTE', self._id, 
+                                       name, type_, value)
             else:
-                raise KeyError("Unknown uniform or attribute %s" % name)
-        except Exception:
-            logger.error("Could not set variable '%s' with value %s" % 
-                         (name, data))
-            raise
-
+                raise KeyError('Cannot set data for a %s.' % kind)
+        else:
+            # This variable is not defined in the current source code,
+            # so we cannot establish whether this is a uniform or
+            # attribute, nor check its type. Try again later.
+            self._pending_variables[name] = data
+    
     def __getitem__(self, name):
-        if name in self._uniforms.keys():
-            return self._uniforms[name].data
-        elif name in self._attributes.keys():
-            return self._attributes[name].data
+        """ Get user-defined data for attributes and uniforms.
+        """
+        if name in self._user_variables:
+            return self._user_variables[name]
+        elif name in self._pending_variables:
+            return self._pending_variables[name]
         else:
             raise KeyError("Unknown uniform or attribute %s" % name)
-
-    @property
-    def all_uniforms(self):
-        """ Program uniforms obtained from shaders code """
-
-        uniforms = []
-        for shader in self._verts:
-            uniforms.extend(shader.uniforms)
-        for shader in self._frags:
-            uniforms.extend(shader.uniforms)
-        uniforms = list(set(uniforms))
-        return uniforms
-
-    @property
-    def active_uniforms(self):
-        """ Program active uniforms obtained from GPU """
-
-        count = gl.glGetProgramParameter(self.handle, gl.GL_ACTIVE_UNIFORMS)
-        # This match a name of the form "name[size]" (= array)
-        regex = re.compile("""(?P<name>\w+)\s*(\[(?P<size>\d+)\])\s*""")
-        uniforms = []
-        for i in range(count):
-            name, size, gtype = gl.glGetActiveUniform(self.handle, i)
-            # This checks if the uniform is an array
-            # Name will be something like xxx[0] instead of xxx
-            m = regex.match(name)
-            # When uniform is an array, size corresponds to the highest used
-            # index
-            if m:
-                name = m.group('name')
-                if size >= 1:
-                    for i in range(size):
-                        name = '%s[%d]' % (m.group('name'), i)
-                        uniforms.append((name, gtype))
-            else:
-                uniforms.append((name, gtype))
-
-        return uniforms
-
-    @property
-    def inactive_uniforms(self):
-        """ Program inactive uniforms obtained from GPU """
-
-        active_uniforms = self.active_uniforms
-        inactive_uniforms = self.all_uniforms
-        for uniform in active_uniforms:
-            if uniform in inactive_uniforms:
-                inactive_uniforms.remove(uniform)
-        return inactive_uniforms
-
-    @property
-    def all_attributes(self):
-        """ Program attributes obtained from shaders code """
-
-        attributes = []
-        for shader in self._verts:
-            attributes.extend(shader.attributes)
-        # No attribute in fragment shaders
-        attributes = list(set(attributes))
-        return attributes
-
-    @property
-    def active_attributes(self):
-        """ Program active attributes obtained from GPU """
-
-        count = gl.glGetProgramParameter(self.handle, gl.GL_ACTIVE_ATTRIBUTES)
-        attributes = []
-
-        # This match a name of the form "name[size]" (= array)
-        regex = re.compile("""(?P<name>\w+)\s*(\[(?P<size>\d+)\])""")
-
-        for i in range(count):
-            name, size, gtype = gl.glGetActiveAttrib(self.handle, i)
-
-            # This checks if the attribute is an array
-            # Name will be something like xxx[0] instead of xxx
-            m = regex.match(name)
-            # When attribute is an array, size corresponds to the highest used
-            # index
-            if m:
-                name = m.group('name')
-                if size >= 1:
-                    for i in range(size):
-                        name = '%s[%d]' % (m.group('name'), i)
-                        attributes.append((name, gtype))
-            else:
-                attributes.append((name, gtype))
-        return attributes
-
-    @property
-    def inactive_attributes(self):
-        """ Program inactive attributes obtained from GPU """
-
-        active_attributes = self.active_attributes
-        inactive_attributes = self.all_attributes
-        for attribute in active_attributes:
-            if attribute in inactive_attributes:
-                inactive_attributes.remove(attribute)
-        return inactive_attributes
-
-    @property
-    def shaders(self):
-        """ List of shaders currently attached to this program """
-
-        shaders = []
-        shaders.extend(self._verts)
-        shaders.extend(self._frags)
-        return shaders
-
-    def draw(self, mode=gl.GL_TRIANGLES, indices=None, check_error=True):
+    
+    def draw(self, mode='triangles', indices=None, check_error=True):
         """ Draw the attribute arrays in the specified mode.
 
         Parameters
         ----------
         mode : str | GL_ENUM
-            GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP,
-            GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN
+            'points', 'lines', 'line_strip', 'line_loop', 'triangles',
+            'triangle_strip', or 'triangle_fan'.
         indices : array
             Array of indices to draw.
         check_error:
             Check error after draw.
+        
         """
-        mode = _check_conversion(mode, _known_draw_modes)
-        self.activate()
-        if check_error:  # need to do this after activating, too
-            gl.check_error('Check after draw activation')
-
-        # WARNING: The "list" of values from a dict is not a list (py3k)
-        attributes = list(self._attributes.values())
+        
+        # Invalidate buffer (data has already been sent)
+        self._buffer = None
+        
+        # Check if mode is valid
+        mode = check_enum(mode)
+        if mode not in ['points', 'lines', 'line_strip', 'line_loop',
+                        'triangles', 'triangle_strip', 'triangle_fan']:
+            raise ValueError('Invalid draw mode: %r' % mode)
+        
+        # Check leftover variables, warn, discard them
+        # In GLIR we check whether all attributes are indeed set
+        for name in self._pending_variables:
+            logger.warn('Variable %r is given but not known.' % name)
+        self._pending_variables = {}
+        
+        # Check attribute sizes
+        attributes = [vbo for vbo in self._user_variables.values() 
+                      if isinstance(vbo, DataBuffer)]
         sizes = [a.size for a in attributes]
         if len(attributes) < 1:
             raise RuntimeError('Must have at least one attribute')
@@ -501,30 +438,30 @@ class Program(GLObject):
             msg = '\n'.join(['%s: %s' % (str(a), a.size) for a in attributes])
             raise RuntimeError('All attributes must have the same size, got:\n'
                                '%s' % msg)
-
+        
+        # Get the glir queue that we need now
+        canvas = get_current_canvas()
+        assert canvas is not None
+        
+        # Associate canvas
+        canvas.context.glir.associate(self.glir)
+        
+        # Indexbuffer
         if isinstance(indices, IndexBuffer):
-            indices.activate()
-            logger.debug("Program drawing %d %r (using index buffer)", 
-                         indices.size, mode)
-            gltypes = {np.dtype(np.uint8): gl.GL_UNSIGNED_BYTE,
-                       np.dtype(np.uint16): gl.GL_UNSIGNED_SHORT,
-                       np.dtype(np.uint32): gl.GL_UNSIGNED_INT}
-            gl.glDrawElements(mode, indices.size, gltypes[indices.dtype], None)
-            indices.deactivate()
+            canvas.context.glir.associate(indices.glir)
+            logger.debug("Program drawing %r with index buffer" % mode)
+            gltypes = {np.dtype(np.uint8): 'UNSIGNED_BYTE',
+                       np.dtype(np.uint16): 'UNSIGNED_SHORT',
+                       np.dtype(np.uint32): 'UNSIGNED_INT'}
+            selection = indices.id, gltypes[indices.dtype], indices.size
+            canvas.context.glir.command('DRAW', self._id, mode, selection)
         elif indices is None:
-            #count = (count or attributes[0].size) - first
-            first = 0
-            count = attributes[0].size
-            logger.debug("Program drawing %d %r (no index buffer)", 
-                         count, mode)
-            gl.glDrawArrays(mode, first, count)
+            selection = 0, attributes[0].size
+            logger.debug("Program drawing %r with %r" % (mode, selection))
+            canvas.context.glir.command('DRAW', self._id, mode, selection)
         else:
             raise TypeError("Invalid index: %r (must be IndexBuffer)" %
                             indices)
-
-        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
-        self.deactivate()
-
-        # Check ok
-        if check_error:
-            gl.check_error('Check after drawing completes')
+        
+        # Process GLIR commands
+        canvas.context.flush_commands()
diff --git a/vispy/gloo/shader.py b/vispy/gloo/shader.py
deleted file mode 100644
index c67bcf6..0000000
--- a/vispy/gloo/shader.py
+++ /dev/null
@@ -1,284 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-# -----------------------------------------------------------------------------
-import re
-import os.path
-
-from . import gl
-from ..util import logger
-from .globject import GLObject
-from .texture import GL_SAMPLER_3D
-
-
-# ------------------------------------------------------------ Shader class ---
-class Shader(GLObject):
-    """ Abstract shader class
-    
-    Parameters
-    ----------
-
-    code: str
-        code can be a filename or the actual code
-    """
-
-    _gtypes = {
-        'float':       gl.GL_FLOAT,
-        'vec2':        gl.GL_FLOAT_VEC2,
-        'vec3':        gl.GL_FLOAT_VEC3,
-        'vec4':        gl.GL_FLOAT_VEC4,
-        'int':         gl.GL_INT,
-        'ivec2':       gl.GL_INT_VEC2,
-        'ivec3':       gl.GL_INT_VEC3,
-        'ivec4':       gl.GL_INT_VEC4,
-        'bool':        gl.GL_BOOL,
-        'bvec2':       gl.GL_BOOL_VEC2,
-        'bvec3':       gl.GL_BOOL_VEC3,
-        'bvec4':       gl.GL_BOOL_VEC4,
-        'mat2':        gl.GL_FLOAT_MAT2,
-        'mat3':        gl.GL_FLOAT_MAT3,
-        'mat4':        gl.GL_FLOAT_MAT4,
-        #        'sampler1D':   gl.GL_SAMPLER_1D,
-        'sampler2D':   gl.GL_SAMPLER_2D,
-        'sampler3D':   GL_SAMPLER_3D,
-    }
-
-    def __init__(self, target, code=None):
-        GLObject.__init__(self)
-        if target not in [gl.GL_VERTEX_SHADER, gl.GL_FRAGMENT_SHADER]:
-            raise ValueError("Shader target must be vertex or fragment")
-
-        self._target = target
-        self._code = None
-        self._source = None
-        self._need_compile = False
-        self.__clean_code = None
-        self._attributes = None
-        self._uniforms = None
-        if code is not None:
-            self.code = code
-
-    @property
-    def code(self):
-        """ Shader source code """
-        return self._code
-
-    @code.setter
-    def code(self, code):
-        """ Shader source code """
-        if os.path.isfile(code):
-            with open(code, 'rt') as file:
-                self._code = file.read()
-                self._source = os.path.basename(code)
-        else:
-            self._code = code
-            self._source = '<string>'
-        self._need_compile = True
-        self.__clean_code = None
-        self._attributes = None
-        self._uniforms = None
-
-    @property
-    def source(self):
-        """ Shader source (string or filename) """
-        return self._source
-    
-    def _activate(self):
-        # shaders do not need any kind of (de)activation
-        # in the sense of glActiveSomething()
-        
-        # Recompile if necessary
-        if self._need_compile:
-            self._compile_shader()
-            self._need_compile = False
-    
-    def _deactivate(self):
-        pass  # shaders do not need any kind of (de)activation
-    
-    def _create(self):
-        """ Create the shader object on the GPU """
-
-        # Check if we have something to compile
-        if not self._code:
-            raise RuntimeError("No code has been given")
-
-        # Create and check that shader object has been created
-        self._handle = gl.glCreateShader(self._target)
-        if self._handle <= 0:
-            raise RuntimeError("Cannot create shader object")
-
-    def _compile_shader(self):
-        """ Compile the source and checks everything's ok """
-        # Set shader source
-        gl.glShaderSource(self._handle, self._code)
-
-        logger.debug("GPU: Creating shader")
-
-        # Actual compilation
-        gl.glCompileShader(self._handle)
-        status = gl.glGetShaderParameter(self._handle, gl.GL_COMPILE_STATUS)
-        if not status:
-            errors = gl.glGetShaderInfoLog(self._handle)
-            errormsg = self._get_error(errors, 4)
-            raise RuntimeError("Shader compilation error in %r:\n%s" % 
-                               (self, errormsg))
-
-    def _delete(self):
-        """ Delete shader from GPU memory (if it was present). """
-
-        gl.glDeleteShader(self._handle)
-
-    def _parse_error(self, error):
-        """
-        Parses a single GLSL error and extracts the line number and error
-        description.
-
-        Parameters
-        ----------
-        error : str
-            An error string as returned byt the compilation process
-        """
-        error = str(error)
-        
-        # Nvidia
-        # 0(7): error C1008: undefined variable "MV"
-        m = re.match(r'(\d+)\((\d+)\)\s*:\s(.*)', error)
-        if m:
-            return int(m.group(2)), m.group(3)
-
-        # ATI / Intel
-        # ERROR: 0:131: '{' : syntax error parse error
-        m = re.match(r'ERROR:\s(\d+):(\d+):\s(.*)', error)
-        if m:
-            return int(m.group(2)), m.group(3)
-
-        # Nouveau
-        # 0:28(16): error: syntax error, unexpected ')', expecting '('
-        m = re.match(r'(\d+):(\d+)\((\d+)\):\s(.*)', error)
-        if m:
-            return int(m.group(2)), m.group(4)
-
-        # Other ...
-        return None, error
-
-    def _get_error(self, errors, indentation=0):
-        """Get error and show the faulty line + some context
-
-        Parameters
-        ----------
-        error : str
-            An error string as returned by the compilation process
-        indentation : int
-            Number of spaces to indent the found error.
-        """
-        # Init
-        results = []
-        lines = None
-        if self._code:
-            lines = [line.strip() for line in self._code.split('\n')]
-
-        for error in errors.split('\n'):
-            # Strip; skip empy lines
-            error = error.strip()
-            if not error:
-                continue
-            # Separate line number from description (if we can)
-            linenr, error = self._parse_error(error)
-            if None in (linenr, lines):
-                results.append('%s' % error)
-            else:
-                results.append('on line %i: %s' % (linenr, error))
-                if linenr > 0 and linenr < len(lines):
-                    results.append('  %s' % lines[linenr - 1])
-
-        # Add indentation and return
-        results = [' ' * indentation + r for r in results]
-        return '\n'.join(results)
-
-    @property
-    def uniforms(self):
-        """ Shader uniforms obtained from source code """
-        if self._uniforms is None:
-            uniforms = []
-            regex = re.compile("""\s*uniform\s+(?P<type>\w+)\s+"""
-                               """(?P<name>\w+)\s*(\[(?P<size>\d+)\])?\s*;""",
-                               flags=re.MULTILINE)
-            for m in re.finditer(regex, self._clean_code):
-                size = -1
-                gtype = Shader._gtypes[m.group('type')]
-                if m.group('size'):
-                    size = int(m.group('size'))
-                if size >= 1:
-                    for i in range(size):
-                        name = '%s[%d]' % (m.group('name'), i)
-                        uniforms.append((name, gtype))
-                else:
-                    uniforms.append((m.group('name'), gtype))
-            self._uniforms = uniforms
-        return self._uniforms
-
-    @property
-    def attributes(self):
-        """ Shader attributes obtained from source code """
-        if self._attributes is None:
-            attributes = []
-            regex = re.compile("""\s*attribute\s+(?P<type>\w+)\s+"""
-                               """(?P<name>\w+)\s*(\[(?P<size>\d+)\])?\s*;""",
-                               flags=re.MULTILINE)
-            for m in re.finditer(regex, self._clean_code):
-                size = -1
-                gtype = Shader._gtypes[m.group('type')]
-                if m.group('size'):
-                    size = int(m.group('size'))
-                if size >= 1:
-                    for i in range(size):
-                        name = '%s[%d]' % (m.group('name'), i)
-                        attributes.append((name, gtype))
-                else:
-                    attributes.append((m.group('name'), gtype))
-            self._attributes = attributes
-        return self._attributes
-
-    @property
-    def _clean_code(self):
-        # Return code with comments stripped
-        if self.__clean_code is None:
-            self.__clean_code = re.sub(r'(.*)(//.*)', r'\1', self._code, re.M)
-        return self.__clean_code
-
-
-# ------------------------------------------------------ VertexShader class ---
-class VertexShader(Shader):
-    """ Vertex shader object
-    
-    Parameters
-    ----------
-
-    code: str
-        code can be a filename or the actual code
-    """
-
-    def __init__(self, code=None):
-        Shader.__init__(self, gl.GL_VERTEX_SHADER, code)
-
-    def __repr__(self):
-        return "Vertex Shader %d (%s)" % (self._id, self._source)
-
-
-# ---------------------------------------------------- FragmentShader class ---
-class FragmentShader(Shader):
-    """ Fragment shader object
-    
-    Parameters
-    ----------
-
-    code: str
-        code can be a filename or the actual code
-    """
-
-    def __init__(self, code=None):
-        Shader.__init__(self, gl.GL_FRAGMENT_SHADER, code)
-
-    def __repr__(self):
-        return "Fragment Shader %d (%s)" % (self._id, self._source)
diff --git a/vispy/gloo/tests/test_buffer.py b/vispy/gloo/tests/test_buffer.py
index aa1d54f..016bdff 100644
--- a/vispy/gloo/tests/test_buffer.py
+++ b/vispy/gloo/tests/test_buffer.py
@@ -6,9 +6,9 @@
 import unittest
 import numpy as np
 
-from vispy.util import use_log_level
-from vispy.gloo import gl
-from vispy.gloo.buffer import Buffer, DataBuffer, VertexBuffer, IndexBuffer
+from vispy.testing import run_tests_if_main
+from vispy.gloo.buffer import (Buffer, DataBuffer, DataBufferView, 
+                               VertexBuffer, IndexBuffer)
 
 
 # -----------------------------------------------------------------------------
@@ -17,57 +17,67 @@ class BufferTest(unittest.TestCase):
     # Default init
     # ------------
     def test_init_default(self):
+        """ Test buffer init"""
+        
+        # No data
         B = Buffer()
-        assert B._target == gl.GL_ARRAY_BUFFER
-        assert B._handle == -1
-        assert B._need_create is True
-        assert B._need_delete is False
-        assert B._nbytes == 0
-        assert B._usage == gl.GL_DYNAMIC_DRAW
-
-    # Unknown target
-    # --------------
-    def test_init_wrong_target(self):
-        # with self.assertRaises(ValueError):
-        #    B = Buffer(target=-1)
-        self.assertRaises(ValueError, Buffer, target=-1)
-
-    # No data
-    # -------
-    def test_init_no_data(self):
-        B = Buffer()
-        assert len(B._pending_data) == 0
-
-    # Data
-    # ----
-    def test_init_with_data(self):
+        assert B.nbytes == 0
+        glir_cmd = B._glir.clear()[-1]
+        assert glir_cmd[0] == 'CREATE'
+        
+        # With data
         data = np.zeros(100)
         B = Buffer(data=data)
-        assert len(B._pending_data) == 1
+        assert B.nbytes == data.nbytes
+        glir_cmd = B._glir.clear()[-1]
+        assert glir_cmd[0] == 'DATA'
+        
+        # With nbytes
+        B = Buffer(nbytes=100)
+        assert B.nbytes == 100
+        glir_cmd = B._glir.clear()[-1]
+        assert glir_cmd[0] == 'SIZE'
+        
+        # Wrong data
+        self.assertRaises(ValueError, Buffer, data, 4)
+        self.assertRaises(ValueError, Buffer, data, data.nbytes)
 
     # Check setting the whole buffer clear pending operations
     # -------------------------------------------------------
     def test_set_whole_data(self):
         data = np.zeros(100)
         B = Buffer(data=data)
+        B._glir.clear()
         B.set_data(data=data)
-        assert len(B._pending_data) == 1
-
+        glir_cmds = B._glir.clear()
+        assert len(glir_cmds) == 2
+        assert glir_cmds[0][0] == 'SIZE'
+        assert glir_cmds[1][0] == 'DATA'
+    
+        # And sub data
+        B.set_subdata(data[:50], 20)
+        glir_cmds = B._glir.clear()
+        assert len(glir_cmds) == 1
+        assert glir_cmds[0][0] == 'DATA'
+        assert glir_cmds[0][2] == 20  # offset
+        
+        # And sub data
+        B.set_subdata(data)
+        glir_cmds = B._glir.clear()
+        assert glir_cmds[-1][0] == 'DATA'
+        
+        # Wrong ways to set subdata
+        self.assertRaises(ValueError, B.set_subdata, data[:50], -1)  # neg
+        self.assertRaises(ValueError, B.set_subdata, data, 10)  # no fit
+        
     # Check stored data is data
     # -------------------------
     def test_data_storage(self):
         data = np.zeros(100)
         B = Buffer(data=data)
         B.set_data(data=data[:50], copy=False)
-        assert B._pending_data[-1][0].base is data
-
-    # Check stored data is a copy
-    # ----------------------------
-    def test_data_copy(self):
-        data = np.zeros(100)
-        B = Buffer(data=data)
-        B.set_data(data=data[:50], copy=True)
-        assert B._pending_data[-1][0].base is not data
+        glir_cmd = B._glir.clear()[-1]
+        assert glir_cmd[-1].base is data
 
     # Check setting oversized data
     # ----------------------------
@@ -123,15 +133,16 @@ class DataBufferTest(unittest.TestCase):
         # Check default storage and copy flags
         data = np.ones(100)
         B = DataBuffer(data)
-        assert B._store is True
-        assert B._copied is False
         assert B.nbytes == data.nbytes
         assert B.offset == 0
         assert B.size == 100
         assert B.itemsize == data.itemsize
         assert B.stride == data.itemsize
         assert B.dtype == data.dtype
-
+        
+        # Given data must be actual numeric data
+        self.assertRaises(TypeError, DataBuffer, 'this is not nice data')
+    
     # Default init with structured data
     # ---------------------------------
     def test_structured_init(self):
@@ -147,38 +158,14 @@ class DataBufferTest(unittest.TestCase):
         assert B.itemsize == data.itemsize
         assert B.stride == data.itemsize
         assert B.dtype == data.dtype
-
-    # CPU storage
-    # ------------
-    def test_storage(self):
-        data = np.ones(100)
-        B = DataBuffer(data, store=True)
-        assert B.data.base is data
-
-    # Use CPU storage but make a local copy for storage
-    # -------------------------------------------------
-    def test_storage_copy(self):
-        data = np.ones(100, np.float32)
-        B = DataBuffer(data.copy(), store=True)  # we got rid of copy arg
-        assert B.data is not None
-        assert B.data is not data
-        assert B.stride == 4
-
+        
     # No CPU storage
     # --------------
     def test_no_storage_copy(self):
         data = np.ones(100, np.float32)
-        B = DataBuffer(data, store=False)
-        assert B.data is None
+        B = DataBuffer(data)
         assert B.stride == 4
 
-    # Empty init (not allowed)
-    # ------------------------
-    def test_empty_init(self):
-        # with self.assertRaises(ValueError):
-        #    B = DataBuffer()
-        self.assertRaises(ValueError, DataBuffer)
-
     # Wrong storage
     # -------------
     def test_non_contiguous_storage(self):
@@ -187,16 +174,9 @@ class DataBufferTest(unittest.TestCase):
         data = np.ones(100, np.float32)
         data_given = data[::2]
         
-        with use_log_level('warning', record=True, print_msg=False) as l:
-            B = DataBuffer(data_given, store=True)
-        assert len(l) == 1
-        assert B._data is not data_given
-        assert B.stride == 4
-        
-        B = DataBuffer(data_given, store=False)
-        assert B._data is not data_given
+        B = DataBuffer(data_given)
         assert B.stride == 4*2
-
+    
     # Get buffer field
     # ----------------
     def test_getitem_field(self):
@@ -230,7 +210,7 @@ class DataBufferTest(unittest.TestCase):
         assert Z.stride == (3 + 2 + 4) * np.dtype(np.float32).itemsize
         assert Z.dtype == (np.float32, 4)
 
-    # Get item via index
+    # Get view via index
     # ------------------
     def test_getitem_index(self):
         dtype = np.dtype([('position', np.float32, 3),
@@ -239,13 +219,23 @@ class DataBufferTest(unittest.TestCase):
         data = np.zeros(10, dtype=dtype)
         B = DataBuffer(data)
         Z = B[0:1]
+        assert Z.base == B
+        assert Z.id == B.id
         assert Z.nbytes == 1 * (3 + 2 + 4) * np.dtype(np.float32).itemsize
         assert Z.offset == 0
         assert Z.size == 1
         assert Z.itemsize == (3 + 2 + 4) * np.dtype(np.float32).itemsize
         assert Z.stride == (3 + 2 + 4) * np.dtype(np.float32).itemsize
         assert Z.dtype == B.dtype
-
+        assert 'DataBufferView' in repr(Z)
+        
+        # There's a few things we cannot do with a view
+        self.assertRaises(RuntimeError, Z.set_data, data)
+        self.assertRaises(RuntimeError, Z.set_subdata, data)
+        self.assertRaises(RuntimeError, Z.resize_bytes, 20)
+        self.assertRaises(RuntimeError, Z.__getitem__, 3)
+        self.assertRaises(RuntimeError, Z.__setitem__, 3, data)
+    
     # View get invalidated when base is resized
     # -----------------------------------------
     def test_invalid_view_after_resize(self):
@@ -254,8 +244,10 @@ class DataBufferTest(unittest.TestCase):
                           ('color',    np.float32, 4)])
         data = np.zeros(10, dtype=dtype)
         B = DataBuffer(data)
+        Y = B['position']
         Z = B[5:]
         B.resize_bytes(5)
+        assert Y._valid is False
         assert Z._valid is False
 
     # View get invalidated after setting oversized data
@@ -277,26 +269,14 @@ class DataBufferTest(unittest.TestCase):
                           ('texcoord', np.float32, 2),
                           ('color',    np.float32, 4)])
         data = np.zeros(10, dtype=dtype)
-        B = DataBuffer(data, store=True)
+        B = DataBuffer(data)
         B.set_data(data)
-        assert len(B._pending_data) == 1
-
-    # Set data on view buffer : error
-    # -------------------------------
-    def test_set_data_base_view(self):
-        dtype = np.dtype([('position', np.float32, 3),
-                          ('texcoord', np.float32, 2),
-                          ('color',    np.float32, 4)])
-        data = np.zeros(10, dtype=dtype)
-        B = DataBuffer(data, store=True)
-        # set_data on field is not allowed because set_data
-        # can result in a buffer resize
-
-        # with self.assertRaises(ValueError):
-        #    B['position'].set_data(data)
-        Z = B['position']
-        self.assertRaises(ValueError, Z.set_data, data)
-
+        last_cmd = B._glir.clear()[-1]
+        assert last_cmd[0] == 'DATA'
+        
+        # Extra kwargs are caught
+        self.assertRaises(TypeError, B.set_data, data, foo=4)
+    
     # Check set_data using offset in data buffer
     # ------------------------------------------
     def test_set_data_offset(self):
@@ -305,33 +285,49 @@ class DataBufferTest(unittest.TestCase):
         
         B = DataBuffer(data)
         B.set_subdata(subdata, offset=10)
-        offset = B._pending_data[-1][2]
+        last_cmd = B._glir.clear()[-1]
+        offset = last_cmd[2]
         assert offset == 10*4
-
-    # Setitem + broadcast
-    # ------------------------------------------------------
-    def test_setitem_broadcast(self):
+    
+    def test_getitem(self):
         dtype = np.dtype([('position', np.float32, 3),
                           ('texcoord', np.float32, 2),
                           ('color',    np.float32, 4)])
         data = np.zeros(10, dtype=dtype)
-        B = DataBuffer(data, store=True)
-        B['position'] = 1, 2, 3
-        assert np.allclose(data['position'].ravel(), np.resize([1, 2, 3], 30))
-
-    # Setitem ellipsis
+        B = DataBuffer(data)
+        assert B[1].dtype == dtype
+        assert B[1].size == 1
+        assert B[-1].dtype == dtype
+        assert B[-1].size == 1
+        
+        self.assertRaises(IndexError, B.__getitem__, +999)
+        self.assertRaises(IndexError, B.__getitem__, -999)
+    
+    def test_setitem(self):
+        dtype = np.dtype([('position', np.float32, 3),
+                          ('texcoord', np.float32, 2),
+                          ('color',    np.float32, 4)])
+        data = np.zeros(10, dtype=dtype)
+        B = DataBuffer(data)
+        B[1] = data[0]
+        B[-1] = data[0]
+        B[:5] = data[:5]
+        B[5:0] = data[:5]  # Weird, but we apparently support this
+        B[1] = b''  # Gets conveted into array of dtype. Lists do not work
+        
+        self.assertRaises(IndexError, B.__setitem__, +999, data[0])
+        self.assertRaises(IndexError, B.__setitem__, -999, data[0])
+        self.assertRaises(TypeError, B.__setitem__, [], data[0])
+        
+    # Setitem + broadcast
     # ------------------------------------------------------
-    def test_setitem_ellipsis(self):
+    def test_setitem_broadcast(self):
         dtype = np.dtype([('position', np.float32, 3),
                           ('texcoord', np.float32, 2),
                           ('color',    np.float32, 4)])
-        data1 = np.zeros(10, dtype=dtype)
-        data2 = np.ones(10, dtype=dtype)
-        B = DataBuffer(data1, store=True)
-        B[...] = data2
-        assert np.allclose(data1['position'], data2['position'])
-        assert np.allclose(data1['texcoord'], data2['texcoord'])
-        assert np.allclose(data1['color'], data2['color'])
+        data = np.zeros(10, dtype=dtype)
+        B = DataBuffer(data)
+        self.assertRaises(ValueError, B.__setitem__, 'position', (1, 2, 3))
 
     # Set every 2 item
     # ------------------------------------------------------
@@ -341,11 +337,9 @@ class DataBufferTest(unittest.TestCase):
                           ('color',    np.float32, 4)])
         data1 = np.zeros(10, dtype=dtype)
         data2 = np.ones(10, dtype=dtype)
-        B = DataBuffer(data1, store=True)
-        B[::2] = data2[::2]
-        assert np.allclose(data1['position'][::2], data2['position'][::2])
-        assert np.allclose(data1['texcoord'][::2], data2['texcoord'][::2])
-        assert np.allclose(data1['color'][::2], data2['color'][::2])
+        B = DataBuffer(data1)
+        s = slice(None, None, 2)
+        self.assertRaises(ValueError, B.__setitem__, s, data2[::2])
 
     # Set half the array
     # ------------------------------------------------------
@@ -355,12 +349,15 @@ class DataBufferTest(unittest.TestCase):
                           ('color',    np.float32, 4)])
         data1 = np.zeros(10, dtype=dtype)
         data2 = np.ones(10, dtype=dtype)
-        B = DataBuffer(data1, store=True)
+        B = DataBuffer(data1)
+        B._glir.clear()
         B[:5] = data2[:5]
-        assert np.allclose(data1['position'][:5], data2['position'][:5])
-        assert np.allclose(data1['texcoord'][:5], data2['texcoord'][:5])
-        assert np.allclose(data1['color'][:5], data2['color'][:5])
-        assert len(B._pending_data) == 2
+        glir_cmds = B._glir.clear()
+        assert len(glir_cmds) == 1
+        set_data = glir_cmds[0][-1]
+        assert np.allclose(set_data['position'], data2['position'][:5])
+        assert np.allclose(set_data['texcoord'][:5], data2['texcoord'][:5])
+        assert np.allclose(set_data['color'][:5], data2['color'][:5])
 
     # Set field without storage: error
     # --------------------------------
@@ -369,9 +366,7 @@ class DataBufferTest(unittest.TestCase):
                           ('texcoord', np.float32, 2),
                           ('color',    np.float32, 4)])
         data = np.zeros(10, dtype=dtype)
-        B = DataBuffer(data, store=False)
-        # with self.assertRaises(ValueError):
-        #    B['position'] = 1, 2, 3
+        B = DataBuffer(data)
         self.assertRaises(ValueError,  B.__setitem__, 'position', (1, 2, 3))
 
     # Set every 2 item without storage:  error
@@ -381,7 +376,7 @@ class DataBufferTest(unittest.TestCase):
                           ('texcoord', np.float32, 2),
                           ('color',    np.float32, 4)])
         data = np.zeros(10, dtype=dtype)
-        B = DataBuffer(data, store=False)
+        B = DataBuffer(data)
         # with self.assertRaises(ValueError):
         #    B[::2] = data[::2]
         s = slice(None, None, 2)
@@ -396,17 +391,46 @@ class DataBufferTest(unittest.TestCase):
         B.set_data(data)
         assert B.nbytes == data.nbytes
 
-    # Resize not allowed using ellipsis
-    # --------------------------------
+    # Resize now allowed using ellipsis
+    # -----------------------------
     def test_no_resize_ellipsis(self):
         data = np.zeros(10)
         B = DataBuffer(data=data)
         data = np.zeros(30)
-        # with self.assertRaises(ValueError):
-        #    B[...] = data
         self.assertRaises(ValueError, B.__setitem__, Ellipsis, data)
+        
+    # Broadcast when using ellipses
+    def test_broadcast_ellipsis(self):
+        data = np.zeros(10)
+        B = DataBuffer(data=data)
+        data = np.zeros(5)
+        B[Ellipsis] = data
+        glir_cmd = B._glir.clear()[-1]
+        assert glir_cmd[-1].shape == (10,)
 
 
+class DataBufferViewTest(unittest.TestCase):
+    
+    def test_init_view(self):
+        data = np.zeros(10)
+        B = DataBuffer(data=data)
+        
+        V = DataBufferView(B, 1)
+        assert V.size == 1
+        
+        V = DataBufferView(B, slice(0, 5))
+        assert V.size == 5
+        
+        V = DataBufferView(B, slice(5, 0))
+        assert V.size == 5
+        
+        V = DataBufferView(B, Ellipsis)
+        assert V.size == 10
+        
+        self.assertRaises(TypeError, DataBufferView, B, [])
+        self.assertRaises(ValueError, DataBufferView, B, slice(0, 10, 2))
+        
+        
 # -----------------------------------------------------------------------------
 class VertexBufferTest(unittest.TestCase):
 
@@ -414,10 +438,45 @@ class VertexBufferTest(unittest.TestCase):
     # -------------------------------
     def test_init_allowed_dtype(self):
         for dtype in (np.uint8, np.int8, np.uint16, np.int16, np.float32):
-            V = VertexBuffer(dtype=dtype)
+            V = VertexBuffer(np.zeros((10, 3), dtype=dtype))
             names = V.dtype.names
             assert V.dtype[names[0]].base == dtype
-            assert V.dtype[names[0]].shape == ()
+            assert V.dtype[names[0]].shape == (3,)
+        for dtype in (np.float64, np.int64):
+            self.assertRaises(TypeError, VertexBuffer,
+                              np.zeros((10, 3), dtype=dtype))
+
+        # Tuple/list is also allowed
+        V = VertexBuffer([1, 2, 3])
+        assert V.size == 3
+        assert V.itemsize == 4
+        #
+        V = VertexBuffer([[1, 2], [3, 4], [5, 6]])
+        assert V.size == 3
+        assert V.itemsize == 2 * 4
+        
+        # Convert
+        data = np.zeros((10,), 'uint8')
+        B = VertexBuffer(data)
+        assert B.dtype[0].base == np.uint8
+        assert B.dtype[0].itemsize == 1
+        #
+        data = np.zeros((10, 2), 'uint8')
+        B = VertexBuffer(data)
+        assert B.dtype[0].base == np.uint8
+        assert B.dtype[0].itemsize == 2
+        B.set_data(data, convert=True)
+        assert B.dtype[0].base == np.float32
+        assert B.dtype[0].itemsize == 8
+        B = VertexBuffer(data[::2].copy())
+        
+        # This is converted to 1D
+        B = VertexBuffer([[1, 2, 3, 4, 5], [1, 2, 3, 4, 5]])
+        assert B.size == 10 
+         
+        # Not allowed
+        self.assertRaises(TypeError, VertexBuffer, dtype=np.float64)
+        #self.assertRaises(TypeError, VertexBuffer, [[1,2,3,4,5],[1,2,3,4,5]])
 
     # VertexBuffer not allowed base types
     # -----------------------------------
@@ -427,26 +486,68 @@ class VertexBufferTest(unittest.TestCase):
             #    V = VertexBuffer(dtype=dtype)
             self.assertRaises(TypeError, VertexBuffer, dtype=dtype)
 
-# -----------------------------------------------------------------------------
+    def test_glsl_type(self):
+        
+        data = np.zeros((10,), np.float32)
+        B = VertexBuffer(data)
+        C = B[1:]
+        assert B.glsl_type == ('attribute', 'float')
+        assert C.glsl_type == ('attribute', 'float')
+        
+        data = np.zeros((10, 2), np.float32)
+        B = VertexBuffer(data)
+        C = B[1:]
+        assert B.glsl_type == ('attribute', 'vec2')
+        assert C.glsl_type == ('attribute', 'vec2')
+        
+        data = np.zeros((10, 4), np.float32)
+        B = VertexBuffer(data)
+        C = B[1:]
+        assert B.glsl_type == ('attribute', 'vec4')
+        assert C.glsl_type == ('attribute', 'vec4')
 
 
+# -----------------------------------------------------------------------------
 class IndexBufferTest(unittest.TestCase):
 
     # IndexBuffer allowed base types
     # ------------------------------
     def test_init_allowed_dtype(self):
+        
+        # allowed dtypes
         for dtype in (np.uint8, np.uint16, np.uint32):
-            V = IndexBuffer(dtype=dtype)
-            assert V.dtype == dtype
+            b = IndexBuffer(np.zeros(10, dtype=dtype))
+            b.dtype == dtype
 
-    # IndexBuffer not allowed base types
-    # -----------------------------------
-    def test_init_not_allowed_dtype(self):
+        # no data => no dtype
+        V = IndexBuffer()
+        V.dtype is None
+        
+        # Not allowed dtypes
         for dtype in (np.int8, np.int16, np.int32,
                       np.float16, np.float32, np.float64):
             # with self.assertRaises(TypeError):
             #    V = IndexBuffer(dtype=dtype)
-            self.assertRaises(TypeError, IndexBuffer, dtype=dtype)
+            data = np.zeros(10, dtype=dtype)
+            self.assertRaises(TypeError, IndexBuffer, data)
+        
+        # Prepare some data
+        dtype = np.dtype([('position', np.float32, 3),
+                          ('texcoord', np.float32, 2), ])
+        sdata = np.zeros(10, dtype=dtype)
+        
+        # Normal data is
+        data = np.zeros([1, 2, 3], np.uint8)
+        B = IndexBuffer(data)
+        assert B.dtype == np.uint8
+        
+        # We can also convert
+        B.set_data(data, convert=True)
+        assert B.dtype == np.uint32
+        
+        # Structured data not allowed
+        self.assertRaises(TypeError, IndexBuffer, dtype=dtype)
+        self.assertRaises(TypeError, B.set_data, sdata)
+
 
-if __name__ == "__main__":
-    unittest.main()
+run_tests_if_main()
diff --git a/vispy/gloo/tests/test_context.py b/vispy/gloo/tests/test_context.py
new file mode 100644
index 0000000..3814984
--- /dev/null
+++ b/vispy/gloo/tests/test_context.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+
+import gc
+
+from vispy.testing import (assert_in, run_tests_if_main, assert_raises,
+                           assert_equal, assert_not_equal)
+
+from vispy import gloo
+from vispy.gloo import (GLContext, get_default_config)
+
+
+class DummyCanvas(object):
+    
+    @property
+    def glir(self):
+        return self
+    
+    def command(self, *args):
+        pass
+
+
+class DummyCanvasBackend(object):
+    
+    def __init__(self):
+        self.set_current = False
+        self._vispy_canvas = DummyCanvas()
+        
+    def _vispy_set_current(self):
+        self.set_current = True
+
+
+def test_context_config():
+    """ Test GLContext handling of config dict
+    """
+    default_config = get_default_config()
+    
+    # Pass default config unchanged
+    c = GLContext(default_config)
+    assert_equal(c.config, default_config)
+    # Must be deep copy
+    c.config['double_buffer'] = False
+    assert_not_equal(c.config, default_config)
+    
+    # Passing nothing should yield default config
+    c = GLContext()
+    assert_equal(c.config, default_config)
+    # Must be deep copy
+    c.config['double_buffer'] = False
+    assert_not_equal(c.config, default_config)
+    
+    # This should work
+    c = GLContext({'red_size': 4, 'double_buffer': False})
+    assert_equal(c.config.keys(), default_config.keys())
+    
+    # Passing crap should raise
+    assert_raises(KeyError, GLContext, {'foo': 3})
+    assert_raises(TypeError, GLContext, {'double_buffer': 'not_bool'})
+
+
+def test_context_taking():
+    """ Test GLContext ownership and taking
+    """
+    def get_canvas(c):
+        return c.shared.ref
+    
+    cb = DummyCanvasBackend()
+    c = GLContext()
+    
+    # Context is not taken and cannot get backend_canvas
+    assert c.shared.name is None
+    assert_raises(RuntimeError, get_canvas, c)
+    assert_in('None backend', repr(c.shared))
+    
+    # Take it
+    c.shared.add_ref('test-foo', cb)
+    assert c.shared.ref is cb
+    assert_in('test-foo backend', repr(c.shared))
+    
+    # Now we can take it again
+    c.shared.add_ref('test-foo', cb)
+    assert len(c.shared._refs) == 2
+    #assert_raises(RuntimeError, c.take, 'test', cb)
+    
+    # Canvas backend can delete (we use a weak ref)
+    cb = DummyCanvasBackend()  # overwrite old object
+    gc.collect()
+    
+    # No more refs
+    assert_raises(RuntimeError, get_canvas, c)
+
+
+def test_gloo_without_app():
+    """ Test gloo without vispy.app (with FakeCanvas) """
+    
+    # Create dummy parser
+    class DummyParser(gloo.glir.BaseGlirParser):
+        def __init__(self):
+            self.commands = []
+        
+        def parse(self, commands):
+            self.commands.extend(commands)
+    
+    p = DummyParser()
+    
+    # Create fake canvas and attach our parser
+    c = gloo.context.FakeCanvas()
+    c.context.shared.parser = p
+    
+    # Do some commands
+    gloo.clear()
+    c.flush()
+    gloo.clear()
+    c.flush()
+    
+    assert len(p.commands) in (2, 3)  # there may be a CURRENT command
+    assert p.commands[-1][1] == 'glClear'
+
+
+run_tests_if_main()
diff --git a/vispy/gloo/tests/test_framebuffer.py b/vispy/gloo/tests/test_framebuffer.py
new file mode 100644
index 0000000..2edf0ed
--- /dev/null
+++ b/vispy/gloo/tests/test_framebuffer.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+
+from vispy.testing import run_tests_if_main, assert_raises
+
+from vispy import gloo
+from vispy.gloo import FrameBuffer, RenderBuffer
+
+
+def test_renderbuffer():
+    
+    # Set with no args
+    assert_raises(ValueError, RenderBuffer)
+    
+    # Set shape only
+    R = RenderBuffer((10, 20))
+    assert R.shape == (10, 20)
+    assert R.format is None
+    
+    # Set both shape and format
+    R = RenderBuffer((10, 20), 'color')
+    assert R.shape == (10, 20)
+    assert R.format is 'color'
+    #
+    glir_cmds = R._glir.clear()
+    assert len(glir_cmds) == 2
+    assert glir_cmds[0][0] == 'CREATE'
+    assert glir_cmds[1][0] == 'SIZE'
+    
+    # Orther formats
+    assert RenderBuffer((10, 20), 'depth').format == 'depth'
+    assert RenderBuffer((10, 20), 'stencil').format == 'stencil'
+    
+    # Test reset size and format
+    R.resize((9, 9), 'depth')
+    assert R.shape == (9, 9)
+    assert R.format == 'depth'
+    R.resize((8, 8), 'stencil')
+    assert R.shape == (8, 8)
+    assert R.format == 'stencil'
+    
+    # Wrong formats
+    assert_raises(ValueError, R.resize, (9, 9), 'no_format')
+    assert_raises(ValueError, R.resize, (9, 9), [])
+    
+    # Resizable
+    R = RenderBuffer((10, 20), 'color', False)
+    assert_raises(RuntimeError, R.resize, (9, 9), 'color')
+    
+    # Attaching sets the format
+    F = FrameBuffer()
+    #
+    R = RenderBuffer((9, 9))
+    F.color_buffer = R
+    assert F.color_buffer is R
+    assert R.format == 'color'
+    #
+    F.depth_buffer = RenderBuffer((9, 9))
+    assert F.depth_buffer.format == 'depth'
+    #
+    F.stencil_buffer = RenderBuffer((9, 9))
+    assert F.stencil_buffer.format == 'stencil'
+
+
+def test_framebuffer():
+    
+    # Test init with no args
+    F = FrameBuffer()
+    glir_cmds = F._glir.clear()
+    assert len(glir_cmds) == 1
+    glir_cmds[0][0] == 'CREATE'
+    
+    # Activate / deactivate
+    F.activate()
+    glir_cmd = F._glir.clear()[-1]
+    assert glir_cmd[0] == 'FRAMEBUFFER'
+    assert glir_cmd[2] is True
+    #
+    F.deactivate()
+    glir_cmd = F._glir.clear()[-1]
+    assert glir_cmd[0] == 'FRAMEBUFFER'
+    assert glir_cmd[2] is False
+    #
+    with F:
+        pass
+    glir_cmds = F._glir.clear()
+    assert len(glir_cmds) == 2
+    assert glir_cmds[0][0] == 'FRAMEBUFFER'
+    assert glir_cmds[1][0] == 'FRAMEBUFFER'
+    assert glir_cmds[0][2] is True and glir_cmds[1][2] is False
+    
+    # Init with args
+    R = RenderBuffer((3, 3))
+    F = FrameBuffer(R)
+    assert F.color_buffer is R
+    #
+    R2 = RenderBuffer((3, 3))
+    F.color_buffer = R2
+    assert F.color_buffer is R2
+    
+    # Wrong buffers
+    F = FrameBuffer()
+    assert_raises(TypeError, FrameBuffer.color_buffer.fset, F, 'FOO')
+    assert_raises(TypeError, FrameBuffer.color_buffer.fset, F, [])
+    assert_raises(TypeError, FrameBuffer.depth_buffer.fset, F, 'FOO')
+    assert_raises(TypeError, FrameBuffer.stencil_buffer.fset, F, 'FOO')
+    color_buffer = RenderBuffer((9, 9), 'color')
+    assert_raises(ValueError, FrameBuffer.depth_buffer.fset, F, color_buffer)
+    # But None is allowed!
+    F.color_buffer = None
+    
+    # Shape
+    R1 = RenderBuffer((3, 3))
+    R2 = RenderBuffer((3, 3))
+    R3 = RenderBuffer((3, 3))
+    F = FrameBuffer(R1, R2, R3)
+    assert F.shape == R1.shape 
+    assert R1.format == 'color'
+    assert R2.format == 'depth'
+    assert R3.format == 'stencil'
+    # Resize
+    F.resize((10, 10))
+    assert F.shape == (10, 10)
+    assert F.shape == R1.shape 
+    assert F.shape == R2.shape 
+    assert F.shape == R3.shape 
+    assert R1.format == 'color'
+    assert R2.format == 'depth'
+    assert R3.format == 'stencil'
+    # Shape from any buffer
+    F.color_buffer = None
+    assert F.shape == (10, 10)
+    F.depth_buffer = None
+    assert F.shape == (10, 10)
+    F.stencil_buffer = None
+    assert_raises(RuntimeError, FrameBuffer.shape.fget, F)
+    
+    # Also with Texture luminance
+    T = gloo.Texture2D((20, 30))
+    R = RenderBuffer(T.shape)
+    assert T.format == 'luminance'
+    F = FrameBuffer(T, R)
+    assert F.shape == T.shape[:2]
+    assert F.shape == R.shape
+    assert T.format == 'luminance'
+    assert R.format == 'depth'
+    # Resize
+    F.resize((10, 10))
+    assert F.shape == (10, 10)
+    assert T.shape == (10, 10, 1)
+    assert F.shape == R.shape 
+    assert T.format == 'luminance'
+    assert R.format == 'depth'
+    
+    # Also with Texture RGB
+    T = gloo.Texture2D((20, 30, 3))
+    R = RenderBuffer(T.shape)
+    assert T.format == 'rgb'
+    F = FrameBuffer(T, R)
+    assert F.shape == T.shape[:2]
+    assert F.shape == R.shape
+    assert T.format == 'rgb'
+    assert R.format == 'depth'
+    # Resize
+    F.resize((10, 10))
+    assert F.shape == (10, 10)
+    assert T.shape == (10, 10, 3)
+    assert F.shape == R.shape 
+    assert T.format == 'rgb'
+    assert R.format == 'depth'
+    
+    # Wrong shape in resize
+    assert_raises(ValueError, F. resize, (9, 9, 1))
+    assert_raises(ValueError, F. resize, (9,))
+    assert_raises(ValueError, F. resize, 'FOO')
+
+
+run_tests_if_main()
diff --git a/vispy/gloo/tests/test_glir.py b/vispy/gloo/tests/test_glir.py
new file mode 100644
index 0000000..14de88e
--- /dev/null
+++ b/vispy/gloo/tests/test_glir.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+
+import json
+import tempfile
+
+from vispy import config
+from vispy.app import Canvas
+from vispy.gloo import glir
+from vispy.testing import requires_application, run_tests_if_main
+
+
+def test_queue():
+    q = glir.GlirQueue()
+    parser = glir.GlirParser()
+    
+    # Test adding commands and clear
+    N = 5
+    for i in range(N):
+        q.command('FOO', 'BAR', i)
+    cmds = q.clear()
+    for i in range(N):
+        assert cmds[i] == ('FOO', 'BAR', i)
+    
+    # Test filter 1
+    cmds1 = [('DATA', 1), ('SIZE', 1), ('FOO', 1), ('SIZE', 1), ('FOO', 1), 
+             ('DATA', 1), ('DATA', 1)]
+    cmds2 = [c[0] for c in q._filter(cmds1, parser)]
+    assert cmds2 == ['FOO', 'SIZE', 'FOO', 'DATA', 'DATA']
+    
+    # Test filter 2
+    cmds1 = [('DATA', 1), ('SIZE', 1), ('FOO', 1), ('SIZE', 2), ('SIZE', 2), 
+             ('DATA', 2), ('SIZE', 1), ('FOO', 1), ('DATA', 1), ('DATA', 1)]
+    cmds2 = q._filter(cmds1, parser)
+    assert cmds2 == [('FOO', 1), ('SIZE', 2), ('DATA', 2), ('SIZE', 1), 
+                     ('FOO', 1), ('DATA', 1), ('DATA', 1)]
+
+    # Define shader
+    shader1 = """
+        precision highp float;uniform mediump vec4 u_foo;uniform vec4 u_bar;
+        """.strip().replace(';', ';\n')
+    # Convert for desktop
+    shader2 = q._convert_shaders('desktop', ['', shader1])[1]
+    assert 'highp' not in shader2
+    assert 'mediump' not in shader2
+    assert 'precision' not in shader2
+    
+    # Convert for es2
+    shader3 = q._convert_shaders('es2', ['', shader2])[1]
+    assert 'precision highp float;' in shader3
+
+
+ at requires_application()
+def test_log_parser():
+    glir_file = tempfile.TemporaryFile(mode='r+')
+
+    config.update(glir_file=glir_file)
+    with Canvas() as c:
+        c.context.set_clear_color('white')
+        c.context.clear()
+
+    glir_file.seek(0)
+    lines = glir_file.read().split(',\n')
+
+    assert lines[0][0] == '['
+    lines[0] = lines[0][1:]
+
+    assert lines[-1][-1] == ']'
+    lines[-1] = lines[-1][:-1]
+
+    i = 0
+
+    assert lines[i] == json.dumps(['CURRENT', 0])
+    i += 1
+    # The 'CURRENT' command may have been called multiple times
+    while lines[i] == lines[i - 1]:
+        i += 1
+    assert lines[i] == json.dumps(['FUNC', 'clearColor', 1.0, 1.0, 1.0, 1.0])
+    i += 1
+    assert lines[i] == json.dumps(['FUNC', 'clear', 17664])
+    i += 1
+    assert lines[i] == json.dumps(['FUNC', 'finish'])
+    i += 1
+
+    config.update(glir_file='')
+
+
+# The rest is basically tested via our examples
+    
+run_tests_if_main()
diff --git a/vispy/gloo/tests/test_globject.py b/vispy/gloo/tests/test_globject.py
index de4fd12..927ce32 100644
--- a/vispy/gloo/tests/test_globject.py
+++ b/vispy/gloo/tests/test_globject.py
@@ -3,23 +3,34 @@
 # Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
 # Distributed under the terms of the new BSD License.
 # -----------------------------------------------------------------------------
-import unittest
+
+from vispy.testing import run_tests_if_main
 from vispy.gloo.globject import GLObject
 
 
-# -----------------------------------------------------------------------------
-class GLObjectTest(unittest.TestCase):
+def test_globject():
+    """ Test gl object uinique id and GLIR CREATE command """
+    
+    objects = [GLObject() for i in range(10)]
+    ids = [ob.id for ob in objects]
+    
+    # Verify that each id is unique (test should not care how)
+    assert len(set(ids)) == len(objects)
+    
+    # Verify that glir commands have been created
+    commands = []
+    for ob in objects:
+        commands.extend(ob._glir.clear())
+    assert len(commands) == len(objects)
+    for cmd in commands:
+        assert cmd[0] == 'CREATE'
+    
+    # Delete
+    ob = objects[-1]
+    q = ob._glir  # get it now, because its gone after we delete it
+    ob.delete()
+    cmd = q.clear()[-1]
+    assert cmd[0] == 'DELETE'
 
-    # Default init
-    # ------------
-    def test_init_default(self):
-        O = GLObject()
 
-        assert O._handle == -1
-        assert O._target is None
-        assert O._need_create is True
-        assert O._need_delete is False
-        assert O._id > 0
-        assert O._id == GLObject._idcount
-if __name__ == "__main__":
-    unittest.main()
+run_tests_if_main()
diff --git a/vispy/gloo/tests/test_program.py b/vispy/gloo/tests/test_program.py
index c7903de..59bee2e 100644
--- a/vispy/gloo/tests/test_program.py
+++ b/vispy/gloo/tests/test_program.py
@@ -5,92 +5,282 @@
 # -----------------------------------------------------------------------------
 import unittest
 
-from vispy.gloo import gl
+import numpy as np
+
+from vispy import gloo, app
 from vispy.gloo.program import Program
-from vispy.gloo.shader import VertexShader, FragmentShader
+from vispy.testing import run_tests_if_main, assert_in, requires_application
+from vispy.gloo.context import set_current_canvas, forget_canvas
 
 
-class ProgramTest(unittest.TestCase):
+class DummyParser(gloo.glir.BaseGlirParser):
+    
+    def convert_shaders(self):
+        return 'desktop'
+    
+    def parse(self, commands):
+        pass
 
-    def test_init(self):
-        program = Program()
-        assert program._handle == -1
-        assert program.shaders == []
 
-    def test_delete_no_context(self):
-        program = Program()
-        program.delete()
+class DummyCanvas:
+    
+    def __init__(self):
+        self.context = gloo.context.GLContext()
+        self.context.shared.parser = DummyParser()
+        self.context.glir.flush = lambda *args: None  # No flush
 
-    def test_init_from_string(self):
-        program = Program("A", "B")
-        assert len(program.shaders) == 2
-        assert program.shaders[0].code == "A"
-        assert program.shaders[1].code == "B"
 
-    def test_init_from_shader(self):
-        program = Program(VertexShader("A"), FragmentShader("B"))
-        assert len(program.shaders) == 2
-        assert program.shaders[0].code == "A"
-        assert program.shaders[1].code == "B"
+class ProgramTest(unittest.TestCase):
 
-    def test_unique_shader(self):
-        vert = VertexShader("A")
-        frag = FragmentShader("B")
-        program = Program([vert, vert], [frag, frag, frag])
-        assert len(program.shaders) == 2
+    def test_init(self):
+        
+        # Test ok init, no shaders
+        program = Program()
+        assert program._user_variables == {}
+        assert program._code_variables == {}
+        assert program._pending_variables == {}
+        assert program.shaders == ('', '')
+        
+        # Test ok init, with shader
+        program = Program('A', 'B')
+        assert program.shaders == ('A', 'B')
+        
+        # False inits
+        self.assertRaises(ValueError, Program, 'A', None)
+        self.assertRaises(ValueError, Program, None, 'B')
+        self.assertRaises(ValueError, Program, 3, 'B')
+        self.assertRaises(ValueError, Program, 3, None)
+        self.assertRaises(ValueError, Program, 'A', 3)
+        self.assertRaises(ValueError, Program, None, 3)
+        self.assertRaises(ValueError, Program, "", "")
+        self.assertRaises(ValueError, Program, "foo", "")
+        self.assertRaises(ValueError, Program, "", "foo")
+    
+    def test_setting_shaders(self):
+        program = Program("A", "B")
+        assert program.shaders[0] == "A"
+        assert program.shaders[1] == "B"
+        
+        program.set_shaders('C', 'D')
+        assert program.shaders[0] == "C"
+        assert program.shaders[1] == "D"
+        
+    @requires_application()
+    def test_error(self):
+        vert = '''
+        void main() {
+            vec2 xy;
+            error on this line
+            vec2 ab;
+        }
+        '''
+        frag = 'void main() { glFragColor = vec4(1, 1, 1, 1); }'
+        with app.Canvas() as c:
+            program = Program(vert, frag)
+            try:
+                program._glir.flush(c.context.shared.parser)
+            except Exception as err:
+                assert_in('error on this line', str(err))
+            else:
+                raise Exception("Compile program should have failed.")
 
     def test_uniform(self):
-        vert = VertexShader("uniform float A;")
-        frag = FragmentShader("uniform float A; uniform vec4 B;")
-        program = Program(vert, frag)
-        assert ("A", gl.GL_FLOAT) in program.all_uniforms
-        assert ("B", gl.GL_FLOAT_VEC4) in program.all_uniforms
-        assert len(program.all_uniforms) == 2
+        
+        # Text array unoforms
+        program = Program("uniform float A[10];", "foo")
+        assert ('uniform_array', 'float', 'A') in program.variables
+        assert len(program.variables) == 11  # array plus elements
+        self.assertRaises(ValueError, program.__setitem__, 'A',
+                          np.ones((9, 1)))
+        program['A'] = np.ones((10, 1))
+        program['A[0]'] = 0
+        assert 'A[0]' in program._user_variables
+        assert 'A[0]' not in program._pending_variables
+        
+        # Init program
+        program = Program("uniform float A;", 
+                          "uniform float A; uniform vec4 B;")
+        assert ('uniform', 'float', 'A') in program.variables
+        assert ('uniform', 'vec4', 'B') in program.variables
+        assert len(program.variables) == 2
+        
+        # Set existing uniforms
+        program['A'] = 3.0
+        assert isinstance(program['A'], np.ndarray)
+        assert program['A'] == 3.0
+        assert 'A' in program._user_variables
+        #
+        program['B'] = 1.0, 2.0, 3.0, 4.0
+        assert isinstance(program['B'], np.ndarray)
+        assert all(program['B'] == np.array((1.0, 2.0, 3.0, 4.0), np.float32))
+        assert 'B' in program._user_variables
+        
+        # Set non-existent uniforms
+        program['C'] = 1.0, 2.0
+        assert program['C'] == (1.0, 2.0)
+        assert 'C' not in program._user_variables
+        assert 'C' in program._pending_variables
+        
+        # Set samplers
+        program.set_shaders("""uniform sampler1D T1;
+                            uniform sampler2D T2;
+                            uniform sampler3D T3;""", "f")
+        program['T1'] = np.zeros((10, ), np.float32)
+        program['T2'] = np.zeros((10, 10), np.float32)
+        program['T3'] = np.zeros((10, 10, 10), np.float32)
+        assert isinstance(program['T1'], gloo.Texture1D)
+        assert isinstance(program['T2'], gloo.Texture2D)
+        assert isinstance(program['T3'], gloo.Texture3D)
+        
+        # Set samplers with textures
+        tex = gloo.Texture2D((10, 10))
+        program['T2'] = tex
+        assert program['T2'] is tex
+        program['T2'] = np.zeros((10, 10), np.float32)  # Update texture
+        assert program['T2'] is tex
+        
+        # C should be taken up when code comes along that mentions it
+        program.set_shaders("uniform float A; uniform vec2 C;", 
+                            "uniform float A; uniform vec4 B;")
+        assert isinstance(program['C'], np.ndarray)
+        assert all(program['C'] == np.array((1.0, 2.0), np.float32))
+        assert 'C' in program._user_variables
+        assert 'C' not in program._pending_variables
 
+        # Set wrong values
+        self.assertRaises(ValueError, program.__setitem__, 'A', (1.0, 2.0))
+        self.assertRaises(ValueError, program.__setitem__, 'B', (1.0, 2.0))
+        self.assertRaises(ValueError, program.__setitem__, 'C', 1.0)
+        
+        # Set wrong values beforehand
+        program['D'] = 1.0, 2.0
+        self.assertRaises(ValueError, program.set_shaders, 
+                          '', 'uniform vec3 D;')
+    
     def test_attributes(self):
-        vert = VertexShader("attribute float A;")
-        frag = FragmentShader("")
-        program = Program(vert, frag)
-        assert program.all_attributes == [("A", gl.GL_FLOAT)]
-
-    def test_attach(self):
-        vert = VertexShader("A")
-        frag = FragmentShader("B")
-        program = Program(vert)
-        program.attach(frag)
-        assert len(program.shaders) == 2
-        assert program.shaders[0].code == "A"
-        assert program.shaders[1].code == "B"
-
-    def test_detach(self):
-        vert = VertexShader("A")
-        frag = FragmentShader("B")
-        program = Program(vert, frag)
-        program.detach(frag)
-        assert len(program.shaders) == 1
-        assert program.shaders[0].code == "A"
-
-    def test_failed_build(self):
-        vert = VertexShader("A")
-        frag = FragmentShader("B")
-
-        program = Program(vert=vert)
-        program._need_create = False  # fool program that it already exists
-        self.assertRaises(ValueError, program.activate)
-
-        program = Program(frag=frag)
-        program._need_create = False  # fool program that it already exists
-        self.assertRaises(ValueError, program.activate)
-
-    def test_setitem(self):
-        vert = VertexShader("")
-        frag = FragmentShader("")
+        program = Program("attribute float A; attribute vec4 B;", "foo")
+        assert ('attribute', 'float', 'A') in program.variables
+        assert ('attribute', 'vec4', 'B') in program.variables
+        assert len(program.variables) == 2
+        
+        from vispy.gloo import VertexBuffer
+        vbo = VertexBuffer()
+        
+        # Set existing uniforms
+        program['A'] = vbo
+        assert program['A'] == vbo
+        assert 'A' in program._user_variables
+        assert program._user_variables['A'] is vbo
+        
+        # Set data - update existing vbp
+        program['A'] = np.zeros((10,), np.float32)
+        assert program._user_variables['A'] is vbo
+        
+        # Set data - create new vbo
+        program['B'] = np.zeros((10, 4), np.float32)
+        assert isinstance(program._user_variables['B'], VertexBuffer)
+        
+        # Set non-existent uniforms
+        vbo = VertexBuffer()  # new one since old one is now wrong size
+        program['C'] = vbo
+        assert program['C'] == vbo
+        assert 'C' not in program._user_variables
+        assert 'C' in program._pending_variables
+        
+        # C should be taken up when code comes along that mentions it
+        program.set_shaders("attribute float A; attribute vec2 C;", "foo")
+        assert program['C'] == vbo
+        assert 'C' in program._user_variables
+        assert 'C' not in program._pending_variables
+        
+        # Set wrong values
+        self.assertRaises(ValueError, program.__setitem__, 'A', 'asddas')
+        
+        # Set wrong values beforehand
+        program['D'] = ""
+        self.assertRaises(ValueError, program.set_shaders, 
+                          'attribute vec3 D;', '')
+        
+        # Set to one value per vertex
+        program.set_shaders("attribute float A; attribute vec2 C;", "foo")
+        program['A'] = 1.0
+        assert program['A'] == 1.0
+        program['C'] = 1.0, 2.0 
+        assert all(program['C'] == np.array((1.0, 2.0), np.float32))
+        #
+        self.assertRaises(ValueError, program.__setitem__, 'A', (1.0, 2.0))
+        self.assertRaises(ValueError, program.__setitem__, 'C', 1.0)
+        self.assertRaises(ValueError, program.bind, 'notavertexbuffer')
 
-        program = Program(vert, frag)
-        #with self.assertRaises(ValueError):
-        #    program["A"] = 1
-        self.assertRaises(KeyError, program.__setitem__, "A", 1)
+        program = Program("attribute vec2 C;", "foo")
+        # first code path: no exsting variable
+        self.assertRaises(ValueError, program.__setitem__, 'C',
+                          np.ones((2, 10), np.float32))
+        # second code path: variable exists (VertexBuffer.set_data)
+        program['C'] = np.ones((10, 2), np.float32)
+        self.assertRaises(ValueError, program.__setitem__, 'C',
+                          np.ones((2, 10), np.float32))
 
+    def test_vbo(self):
+        # Test with count
+        program = Program('attribute float a; attribute vec2 b;', 'foo', 10)
+        assert program._count == 10
+        assert ('attribute', 'float', 'a') in program.variables
+        assert ('attribute', 'vec2', 'b') in program.variables
+        
+        # Set
+        program['a'] = np.ones((10,), np.float32)
+        assert np.all(program._buffer['a'] == 1)
+        
+    def test_vayings(self):
+        
+        # Varyings and constants are detected
+        program = Program("varying float A; const vec4 B;", "foo")
+        assert ('varying', 'float', 'A') in program.variables
+        assert ('const', 'vec4', 'B') in program.variables
+        
+        # But cannot be set
+        self.assertRaises(KeyError, program.__setitem__, 'A', 3.0)
+        self.assertRaises(KeyError, program.__setitem__, 'B', (1.0, 2.0, 3.0))
+        # And anything else also fails
+        self.assertRaises(KeyError, program.__getitem__, 'fooo')
+    
+    def test_draw(self):
+        # Init
+        program = Program("attribute float A;", "uniform float foo")
+        program['A'] = np.zeros((10,), np.float32)
+        
+        dummy_canvas = DummyCanvas()
+        glir = dummy_canvas.context.glir
+        set_current_canvas(dummy_canvas)
+        try:
+            # Draw arrays
+            program.draw('triangles')
+            glir_cmd = glir.clear()[-1]
+            assert glir_cmd[0] == 'DRAW'
+            assert len(glir_cmd[-1]) == 2
+            
+            # Draw elements
+            indices = gloo.IndexBuffer(np.zeros(10, dtype=np.uint8))
+            program.draw('triangles', indices)
+            glir_cmd = glir.clear()[-1]
+            assert glir_cmd[0] == 'DRAW'
+            assert len(glir_cmd[-1]) == 3
+            
+            # Invalid mode
+            self.assertRaises(ValueError, program.draw, 'nogeometricshape')
+            # Invalid index
+            self.assertRaises(TypeError, program.draw, 'triangles', 'notindex')
+            # No atributes
+            program = Program("attribute float A;", "uniform float foo")
+            self.assertRaises(RuntimeError, program.draw, 'triangles')
+            # Atributes with different sizes
+            program = Program("attribute float A; attribute float B;", "foo")
+            program['A'] = np.zeros((10,), np.float32)
+            program['B'] = np.zeros((11,), np.float32)
+            self.assertRaises(RuntimeError, program.draw, 'triangles')
+        
+        finally:
+            forget_canvas(dummy_canvas)
 
-if __name__ == "__main__":
-    unittest.main()
+run_tests_if_main()
diff --git a/vispy/gloo/tests/test_shader.py b/vispy/gloo/tests/test_shader.py
deleted file mode 100644
index 46a4565..0000000
--- a/vispy/gloo/tests/test_shader.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
-# Distributed under the terms of the new BSD License.
-# -----------------------------------------------------------------------------
-import unittest
-
-from vispy.gloo import gl
-from vispy.gloo.shader import VertexShader, FragmentShader
-from vispy.testing import assert_in, assert_not_in
-
-
-# -----------------------------------------------------------------------------
-class VertexShaderTest(unittest.TestCase):
-
-    def test_init(self):
-        shader = VertexShader()
-        assert shader._target == gl.GL_VERTEX_SHADER
-
-
-# -----------------------------------------------------------------------------
-class FragmentShaderTest(unittest.TestCase):
-
-    def test_init(self):
-        shader = FragmentShader()
-        assert shader._target == gl.GL_FRAGMENT_SHADER
-
-
-# -----------------------------------------------------------------------------
-class ShaderTest(unittest.TestCase):
-
-    def test_init(self):
-        shader = VertexShader()
-        assert shader._handle == -1
-        assert shader.code is None
-        assert shader.source is None
-
-    def test_sourcecode(self):
-        code = "/* Code */"
-        shader = VertexShader(code)
-        assert shader.code == code
-        assert shader.source == "<string>"
-
-    def test_empty_build(self):
-        shader = VertexShader()
-        #with self.assertRaises(RuntimeError):
-        #    shader.activate()
-        self.assertRaises(RuntimeError, shader.activate)
-
-    def test_delete_no_context(self):
-        shader = VertexShader()
-        shader.delete()
-
-    def test_uniform_float(self):
-        shader = VertexShader("uniform float color;")
-        assert shader.uniforms == [("color", gl.GL_FLOAT)]
-
-    def test_uniform_vec4(self):
-        shader = VertexShader("uniform vec4 color;")
-        assert shader.uniforms == [("color", gl.GL_FLOAT_VEC4)]
-
-    def test_uniform_array(self):
-        shader = VertexShader("uniform float color[2];")
-        assert shader.uniforms == [("color[0]", gl.GL_FLOAT),
-                                   ("color[1]", gl.GL_FLOAT)]
-
-    def test_attribute_float(self):
-        shader = VertexShader("attribute float color;")
-        assert shader.attributes == [("color", gl.GL_FLOAT)]
-
-    def test_attribute_vec4(self):
-        shader = VertexShader("attribute vec4 color;")
-        assert shader.attributes == [("color", gl.GL_FLOAT_VEC4)]
-        
-    def test_ignore_comments(self):
-        shader = VertexShader("""
-            attribute vec4 color; attribute float x;
-            // attribute float y;
-            attribute float z; //attribute float w;
-        """)
-        names = [attr[0] for attr in shader.attributes]
-        assert_in("color", names)
-        assert_in("x", names)
-        assert_in("z", names)
-        assert_not_in("y", names)
-        assert_not_in("w", names)
-
-        
-if __name__ == "__main__":
-    unittest.main()
diff --git a/vispy/gloo/tests/test_texture.py b/vispy/gloo/tests/test_texture.py
index cc6b59e..1f39a8e 100644
--- a/vispy/gloo/tests/test_texture.py
+++ b/vispy/gloo/tests/test_texture.py
@@ -6,9 +6,8 @@
 import unittest
 import numpy as np
 
-from vispy.util import use_log_level
-from vispy.gloo import Texture2D, Texture3D, gl
-from vispy.testing import requires_pyopengl
+from vispy.gloo import Texture1D, Texture2D, Texture3D, TextureAtlas
+from vispy.testing import requires_pyopengl, run_tests_if_main, assert_raises
 
 # here we test some things that will be true of all Texture types:
 Texture = Texture2D
@@ -26,199 +25,35 @@ class TextureTest(unittest.TestCase):
     # ---------------------------------
     def test_init_data(self):
         data = np.zeros((10, 10, 3), dtype=np.uint8)
-        T = Texture(data=data)
+        T = Texture(data=data, interpolation='linear', wrapping='repeat')
         assert T._shape == (10, 10, 3)
-        assert T._dtype == np.uint8
-        assert T._offset == (0, 0, 0)
-        assert T._store is True
-        assert T._copy is False
-        assert T._need_resize is True
-        # assert T._data is data
-        assert len(T._pending_data) == 1
+        assert T._interpolation == ('linear', 'linear')
+        assert T._wrapping == ('repeat', 'repeat')
 
-    # Non contiguous data
-    # ---------------------------------
-    def test_init_non_contiguous_data(self):
-        data = np.zeros((10, 10), dtype=np.uint8)
-        with use_log_level('warning', record=True, print_msg=False) as l:
-            T = Texture(data=data[::2, ::2])
-        assert len(l) == 1
-        assert T._shape == (5, 5, 1)
-        assert T._dtype == np.uint8
-        assert T._offset == (0, 0, 0)
-        assert T._store is True
-        assert T._copy is True
-        assert T._need_resize is True
-        assert T._pending_data
-        assert T._data is not data
-        assert len(T._pending_data) == 1
-
-    # Dtype and shape
+    # Setting data and shape
     # ---------------------------------
     def test_init_dtype_shape(self):
-        T = Texture(shape=(10, 10), dtype=np.uint8)
+        T = Texture((10, 10))
         assert T._shape == (10, 10, 1)
-        assert T._dtype == np.uint8
-        assert T._offset == (0, 0, 0)
-        assert T._store is True
-        assert T._copy is False
-        assert T._need_resize is True
-        assert not T._pending_data
-        assert T._data is not None
-        assert T._data.shape == (10, 10, 1)
-        assert T._data.dtype == np.uint8
-        assert len(T._pending_data) == 0
-        self.assertRaises(ValueError, Texture, shape=(10, 10), dtype=np.bool)
         self.assertRaises(ValueError, Texture, shape=(10, 10),
                           data=np.zeros((10, 10), np.float32))
 
-    # Dtype only
-    # ---------------------------------
-    def test_init_dtype(self):
-        self.assertRaises(ValueError, Texture, dtype=np.uint8)
-
-    # Data and dtype: dtype prevails
-    # ---------------------------------
-    def test_init_data_dtype(self):
-        data = np.zeros((10, 10, 1), dtype=np.uint8)
-        T = Texture(data=data, dtype=np.uint16)
-        assert T._shape == (10, 10, 1)
-        assert T._dtype == np.uint16
-        assert T._offset == (0, 0, 0)
-        assert T._store is True
-        assert T._copy is False
-        assert T._need_resize is True
-        assert T._data is not data
-        assert T._pending_data
-        assert len(T._pending_data) == 1
-
-    # Data, store but no copy
-    # ---------------------------------
-    def test_init_data_store(self):
-        data = np.zeros((10, 10), dtype=np.uint8)
-        Texture(data=data, store=True)
-        # assert T._data is data
-
-    # Data, store and copy
-    # ---------------------------------
-    def test_init_data_store_copy(self):
-        data = np.zeros((10, 10), dtype=np.uint8)
-        T = Texture(data=data.copy(), store=True)
-        assert T._data is not data
-        assert T._data is not None
-
-    # Get a view of the whole texture
-    # ---------------------------------
-    def test_getitem_ellipsis(self):
-
-        data = np.zeros((10, 10, 3), dtype=np.uint8)
-        T = Texture(data=data)
-        Z = T[...]
-        assert Z._base is T
-        # assert Z._data.base is T._data
-        assert Z._shape == (10, 10, 3)
-        assert Z._resizeable is False
-        assert len(Z._pending_data) == 0
-
-    # Get a view restrictions
-    # ---------------------------------
-    def test_view_restrictions(self):
-
-        data = np.zeros((10, 10, 3), dtype=np.uint8)
-        T = Texture(data=data)
-        Z = T[...]
-        self.assertRaises(ValueError, Z.__getitem__, 1)
-        self.assertRaises(RuntimeError, Z.resize, (10, 10, 3))
-        self.assertRaises(ValueError, Z.resize, (10,))
-        T.resize(T.shape)  # noop
-        self.assertRaises(ValueError, Texture.interpolation.fset, Z, 'nearest')
-        assert Z.interpolation is T.interpolation
-        self.assertRaises(ValueError, Texture.wrapping.fset, Z, 'repeat')
-        assert Z.wrapping is T.wrapping
-
-    # Get a view using ellipsis at start
-    # ---------------------------------
-    def test_getitem_ellipsis_start(self):
-
-        data = np.zeros((10, 10, 3), dtype=np.uint8)
-        T = Texture(data=data)
-        Z = T[..., 0]
-        assert Z._base is T
-        # assert Z._data.base is T._data
-        assert Z._shape == (10, 10, 1)
-        assert Z._resizeable is False
-        assert len(Z._pending_data) == 0
-
-    # Get a view using ellipsis at end
-    # ---------------------------------
-    def test_getitem_ellipsis_end(self):
-
-        data = np.zeros((10, 10, 3), dtype=np.uint8)
-        T = Texture(data=data)
-        Z = T[0, ...]
-        assert Z._base is T
-        # assert Z._data.base is T._data
-        assert Z._shape == (1, 10, 3)
-        assert Z._resizeable is False
-        assert len(Z._pending_data) == 0
-
-    # Get a single item
-    # ---------------------------------
-    def test_getitem_single(self):
-
-        data = np.zeros((10, 10, 3), dtype=np.uint8)
-        T = Texture(data=data)
-        Z = T[0, 0, 0]
-        assert Z._base is T
-        # assert Z._data.base is T._data
-        assert Z._shape == (1, 1, 1)
-        assert Z._resizeable is False
-        assert len(Z._pending_data) == 0
-
-    # Get a partial view
-    # ---------------------------------
-    def test_getitem_partial(self):
-
-        data = np.zeros((10, 10), dtype=np.uint8)
-        T = Texture(data=data)
-        Z = T[2:5, 2:5]
-        assert Z._base is T
-        # assert Z._data.base is T._data
-        assert Z._shape == (3, 3, 1)
-        assert Z._offset == (2, 2, 0)
-        assert Z._resizeable is False
-        assert len(Z._pending_data) == 0
-
-    # Get non contiguous view : forbidden
-    # ---------------------------------
-    def test_getitem_non_contiguous(self):
-
-        data = np.zeros((10, 10), dtype=np.uint8)
-        T = Texture(data=data)
-        # with self.assertRaises(ValueError):
-        #    Z = T[::2, ::2]
-        #    print(Z)
-        s = slice(None, None, 2)
-        self.assertRaises(ValueError, T.__getitem__, (s, s))
-
     # Set data with store
     # ---------------------------------
     def test_setitem_all(self):
-
         data = np.zeros((10, 10), dtype=np.uint8)
         T = Texture(data=data)
         T[...] = np.ones((10, 10, 1))
-        assert len(T._pending_data) == 1
-        assert np.allclose(data, np.ones((10, 10, 1)))
+        glir_cmd = T._glir.clear()[-1]
+        assert glir_cmd[0] == 'DATA'
+        assert np.allclose(glir_cmd[3], np.ones((10, 10, 1)))
 
     # Set data without store
     # ---------------------------------
     def test_setitem_all_no_store(self):
-
         data = np.zeros((10, 10), dtype=np.uint8)
-        T = Texture(data=data, store=False)
+        T = Texture(data=data)
         T[...] = np.ones((10, 10), np.uint8)
-        assert len(T._pending_data) == 1
         assert np.allclose(data, np.zeros((10, 10)))
 
     # Set a single data
@@ -228,8 +63,12 @@ class TextureTest(unittest.TestCase):
         data = np.zeros((10, 10), dtype=np.uint8)
         T = Texture(data=data)
         T[0, 0, 0] = 1
-        assert len(T._pending_data) == 2
-        assert data[0, 0] == 1, 1
+        glir_cmd = T._glir.clear()[-1]
+        assert glir_cmd[0] == 'DATA'
+        assert np.allclose(glir_cmd[3], np.array([1]))
+        
+        # We apparently support this
+        T[8:3, 3] = 1
 
     # Set some data
     # ---------------------------------
@@ -238,8 +77,9 @@ class TextureTest(unittest.TestCase):
         data = np.zeros((10, 10), dtype=np.uint8)
         T = Texture(data=data)
         T[5:, 5:] = 1
-        assert len(T._pending_data) == 2
-        assert np.allclose(data[5:, 5:], np.ones((5, 5)))
+        glir_cmd = T._glir.clear()[-1]
+        assert glir_cmd[0] == 'DATA'
+        assert np.allclose(glir_cmd[3], np.ones((5, 5)))
 
     # Set non contiguous data
     # ---------------------------------
@@ -249,32 +89,49 @@ class TextureTest(unittest.TestCase):
         # with self.assertRaises(ValueError):
         #    T[::2, ::2] = 1
         s = slice(None, None, 2)
-        self.assertRaises(ValueError, T.__setitem__, (s, s), 1)
-
-    # Set via get (pending data on base)
-    # ---------------------------------
-    def test_getitem_setitem(self):
-        data = np.zeros((10, 10), dtype=np.uint8)
-        T = Texture(data=data)
-        Z = T[5:, 5:]
-        Z[...] = 1
-        assert len(Z._pending_data) == 0
-        assert len(T._pending_data) == 2
-        assert np.allclose(data[5:, 5:], np.ones((5, 5)))
+        self.assertRaises(IndexError, T.__setitem__, (s, s), 1)
+        self.assertRaises(IndexError, T.__setitem__, (-100, 3), 1)
+        self.assertRaises(TypeError, T.__setitem__, ('foo', 'bar'), 1)
 
     # Set properties
     def test_set_texture_properties(self):
-        T = Texture(shape=(10, 10), dtype=np.float32)
+        T = Texture((10, 10))
+        
+        # Interpolation
+        T.interpolation = 'nearest'
+        assert T.interpolation == 'nearest'
         T.interpolation = 'linear'
-        assert T.interpolation == gl.GL_LINEAR
+        assert T.interpolation == 'linear'
         T.interpolation = ['linear'] * 2
-        assert T.interpolation == gl.GL_LINEAR
+        assert T.interpolation == 'linear'
         T.interpolation = ['linear', 'nearest']
-        assert T.interpolation == (gl.GL_LINEAR, gl.GL_NEAREST)
-        self.assertRaises(ValueError, Texture.interpolation.fset, T,
-                          ['linear'] * 3)
+        assert T.interpolation == ('linear', 'nearest')
+        
+        # Wrong interpolation
+        iset = Texture.interpolation.fset
+        self.assertRaises(ValueError, iset, T, ['linear'] * 3)
+        self.assertRaises(ValueError, iset, T, True)
+        self.assertRaises(ValueError, iset, T, [])
+        self.assertRaises(ValueError, iset, T, 'linearios')
+        
+        # Wrapping
         T.wrapping = 'clamp_to_edge'
-        assert T.wrapping == gl.GL_CLAMP_TO_EDGE
+        assert T.wrapping == 'clamp_to_edge'
+        T.wrapping = 'repeat'
+        assert T.wrapping == 'repeat'
+        T.wrapping = 'mirrored_repeat'
+        assert T.wrapping == 'mirrored_repeat'
+        T.wrapping = 'repeat', 'repeat'
+        assert T.wrapping == 'repeat'
+        T.wrapping = 'repeat', 'clamp_to_edge'
+        assert T.wrapping == ('repeat', 'clamp_to_edge')
+        
+        # Wrong wrapping
+        wset = Texture.wrapping.fset
+        self.assertRaises(ValueError, wset, T, ['repeat'] * 3)
+        self.assertRaises(ValueError, wset, T, True)
+        self.assertRaises(ValueError, wset, T, [])
+        self.assertRaises(ValueError, wset, T, 'repeatos')
 
 
 # --------------------------------------------------------------- Texture2D ---
@@ -287,7 +144,9 @@ class Texture2DTest(unittest.TestCase):
     def test_init(self):
         data = np.zeros((10, 10), dtype=np.uint8)
         T = Texture2D(data=data)
+        assert 'Texture2D' in repr(T)
         assert T._shape == (10, 10, 1)
+        assert T.glsl_type == ('uniform', 'sampler2D')
 
     # Width & height
     # ---------------------------------
@@ -304,11 +163,12 @@ class Texture2DTest(unittest.TestCase):
         T = Texture2D(data=data)
         T.resize((5, 5))
         assert T.shape == (5, 5, 1)
-        assert T._data.shape == (5, 5, 1)
-        assert T._need_resize is True
-        assert not T._pending_data
-        assert len(T._pending_data) == 0
-
+        glir_cmd = T._glir.clear()[-1]
+        assert glir_cmd[0] == 'SIZE'
+        
+        # Wong arg
+        self.assertRaises(ValueError, T.resize, (5, 5), 4)
+    
     # Resize with bad shape
     # ---------------------------------
     def test_resize_bad_shape(self):
@@ -316,23 +176,15 @@ class Texture2DTest(unittest.TestCase):
         T = Texture2D(data=data)
         # with self.assertRaises(ValueError):
         #    T.resize((5, 5, 5))
+        self.assertRaises(ValueError, T.resize, (5,))
         self.assertRaises(ValueError, T.resize, (5, 5, 5))
+        self.assertRaises(ValueError, T.resize, (5, 5, 5, 1))
 
-    # Resize view (forbidden)
+    # Resize not resizable
     # ---------------------------------
-    def test_resize_view(self):
+    def test_resize_unresizable(self):
         data = np.zeros((10, 10), dtype=np.uint8)
-        T = Texture2D(data=data)
-        # with self.assertRaises(RuntimeError):
-        #    T[...].resize((5, 5))
-        Z = T[...]
-        self.assertRaises(RuntimeError, Z.resize, (5, 5))
-
-    # Resize not resizeable
-    # ---------------------------------
-    def test_resize_unresizeable(self):
-        data = np.zeros((10, 10), dtype=np.uint8)
-        T = Texture2D(data=data, resizeable=False)
+        T = Texture2D(data=data, resizable=False)
         # with self.assertRaises(RuntimeError):
         #    T.resize((5, 5))
         self.assertRaises(RuntimeError, T.resize, (5, 5))
@@ -344,8 +196,9 @@ class Texture2DTest(unittest.TestCase):
         T = Texture2D(data=data)
         T.set_data(np.ones((20, 20), np.uint8))
         assert T.shape == (20, 20, 1)
-        assert T._data.shape == (20, 20, 1)
-        assert len(T._pending_data) == 1
+        glir_cmds = T._glir.clear()
+        assert glir_cmds[-2][0] == 'SIZE'
+        assert glir_cmds[-1][0] == 'DATA'
     
     # Set undersized data
     # ---------------------------------
@@ -354,7 +207,9 @@ class Texture2DTest(unittest.TestCase):
         T = Texture2D(data=data)
         T.set_data(np.ones((5, 5), np.uint8))
         assert T.shape == (5, 5, 1)
-        assert len(T._pending_data) == 1
+        glir_cmds = T._glir.clear()
+        assert glir_cmds[-2][0] == 'SIZE'
+        assert glir_cmds[-1][0] == 'DATA'
 
     # Set misplaced data
     # ---------------------------------
@@ -374,7 +229,8 @@ class Texture2DTest(unittest.TestCase):
         # with self.assertRaises(ValueError):
         #    T.set_data(np.ones((10, 10)))
         self.assertRaises(ValueError, T.set_data, np.ones((10,)))
-
+        self.assertRaises(ValueError, T.set_data, np.ones((5, 5, 5, 1)),)
+        
     # Set whole data (clear pending data)
     # ---------------------------------
     def test_set_whole_data(self):
@@ -382,16 +238,6 @@ class Texture2DTest(unittest.TestCase):
         T = Texture2D(data=data)
         T.set_data(np.ones((10, 10), np.uint8))
         assert T.shape == (10, 10, 1)
-        assert len(T._pending_data) == 1
-    
-    # Test view get invalidated when base is resized
-    # ----------------------------------------------
-    def test_invalid_views(self):
-        data = np.zeros((10, 10), dtype=np.uint8)
-        T = Texture2D(data=data)
-        Z = T[5:, 5:]
-        T.resize((5, 5))
-        assert Z._valid is False
     
     # Test set data with different shape
     # ---------------------------------
@@ -403,201 +249,467 @@ class Texture2DTest(unittest.TestCase):
         data = np.zeros((10, 10, 1), dtype=np.uint8)
         T = Texture2D(data=data)
         assert T.shape == (10, 10, 1)
-        assert T._format == gl.GL_LUMINANCE
+        assert T.format == 'luminance'
         
         # Set data to rgb
         T.set_data(np.zeros(shape3, np.uint8))
         assert T.shape == (10, 10, 3)
-        assert T._format == gl.GL_RGB
+        assert T.format == 'rgb'
         
         # Set data to grayscale
         T.set_data(np.zeros(shape1, np.uint8))
         assert T.shape == (10, 10, 1)
-        assert T._format == gl.GL_LUMINANCE
+        assert T.format == 'luminance'
         
         # Set size to rgb
         T.resize(shape3)
         assert T.shape == (10, 10, 3)
-        assert T._format == gl.GL_RGB
+        assert T._format == 'rgb'
         
         # Set size to grayscale
         T.resize(shape1)
         assert T.shape == (10, 10, 1)
-        assert T._format == gl.GL_LUMINANCE
+        assert T._format == 'luminance'
+        
+        # Keep using old format
+        T.resize(shape1, 'alpha')
+        T.resize(shape1)
+        assert T._format == 'alpha'
+        
+        # Use luminance as default
+        T.resize(shape3)
+        T.resize(shape1)
+        assert T._format == 'luminance'
+        
+        # Too large
+        self.assertRaises(ValueError, T.resize, (5, 5, 5, 1))
+        # Cannot determine format
+        self.assertRaises(ValueError, T.resize, (5, 5, 5))
+        # Invalid format
+        self.assertRaises(ValueError, T.resize, shape3, 'foo')
+        self.assertRaises(ValueError, T.resize, shape3, 'alpha')
+        #self.assertRaises(ValueError, T.resize, shape3, 4)
     
-    # Test set data with different shape
-    # ---------------------------------
+    # Test set data with different shape and type
+    # -------------------------------------------
     def test_reset_data_type(self):
-        shape = 10, 10
-        T = Texture2D(data=np.zeros(shape, dtype=np.uint8))
-        assert T.dtype == np.uint8
-        assert T._gtype == gl.GL_UNSIGNED_BYTE
-
-        newdata = np.zeros(shape, dtype=np.float32)
-        self.assertRaises(ValueError, T.set_data, newdata)
-
-        newdata = np.zeros(shape, dtype=np.int32)
-        self.assertRaises(ValueError, T.set_data, newdata)
+        data = np.zeros((10, 10), dtype=np.uint8)
+        T = Texture2D(data=data)
+        
+        data = np.zeros((10, 11), dtype=np.float32)
+        T.set_data(data)
+        
+        data = np.zeros((12, 10), dtype=np.int32)
+        T.set_data(data)
+        
+        self.assertRaises(ValueError, T.set_data, np.zeros([10, 10, 10, 1]))
 
 
-# --------------------------------------------------------------- Texture3D ---
-class Texture3DTest(unittest.TestCase):
+# --------------------------------------------------------------- Texture1D ---
+ at requires_pyopengl()
+def test_texture_1D():
     # Note: put many tests related to (re)sizing here, because Texture
     # is not really aware of shape.
 
-    @requires_pyopengl()
-    def __init__(self, *args, **kwds):
-        unittest.TestCase.__init__(self, *args, **kwds)
-
     # Shape extension
     # ---------------------------------
-    def test_init(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        assert T._shape == (10, 10, 10, 1)
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+    assert T._shape == (10, 1)
+    assert 'Texture1D' in repr(T)
+    assert T.glsl_type == ('uniform', 'sampler1D')
 
-    # Width & height
+    # Width
     # ---------------------------------
-    def test_width_height_depth(self):
-        data = np.zeros((10, 20, 30), dtype=np.uint8)
-        T = Texture3D(data=data)
-        assert T.width == 30
-        assert T.height == 20
-        assert T.depth == 10
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+    assert T.width == 10
 
     # Resize
     # ---------------------------------
-    def test_resize(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        T.resize((5, 5, 5))
-        assert T.shape == (5, 5, 5, 1)
-        assert T._data.shape == (5, 5, 5, 1)
-        assert T._need_resize is True
-        assert not T._pending_data
-        assert len(T._pending_data) == 0
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+    T.resize((5, ))
+    assert T.shape == (5, 1)
+    glir_cmd = T._glir.clear()[-1]
+    assert glir_cmd[0] == 'SIZE'
+    assert glir_cmd[2] == (5, 1)
 
     # Resize with bad shape
     # ---------------------------------
-    def test_resize_bad_shape(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        # with self.assertRaises(ValueError):
-        #    T.resize((5, 5, 5, 5))
-        self.assertRaises(ValueError, T.resize, (5, 5, 5, 5))
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+    # with self.assertRaises(ValueError):
+    #    T.resize((5, 5, 5, 5))
+    assert_raises(ValueError, T.resize, (5, 5, 5, 5))
 
-    # Resize not resizeable
+    # Resize not resizable
     # ---------------------------------
-    def test_resize_unresizeable(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data, resizeable=False)
-        # with self.assertRaises(RuntimeError):
-        #    T.resize((5, 5, 5))
-        self.assertRaises(RuntimeError, T.resize, (5, 5, 5))
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data, resizable=False)
+    # with self.assertRaises(RuntimeError):
+    #    T.resize((5, 5, 5))
+    assert_raises(RuntimeError, T.resize, (5, ))
 
     # Set oversized data (-> resize)
     # ---------------------------------
-    def test_set_oversized_data(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        T.set_data(np.ones((20, 20, 20), np.uint8))
-        assert T.shape == (20, 20, 20, 1)
-        assert T._data.shape == (20, 20, 20, 1)
-        assert len(T._pending_data) == 1
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+    T.set_data(np.ones((20, ), np.uint8))
+    assert T.shape == (20, 1)
 
     # Set undersized data
     # ---------------------------------
-    def test_set_undersized_data(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        T.set_data(np.ones((5, 5, 5), np.uint8))
-        assert T.shape == (5, 5, 5, 1)
-        assert len(T._pending_data) == 1
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+    T.set_data(np.ones((5, ), np.uint8))
+    assert T.shape == (5, 1)
 
     # Set misplaced data
     # ---------------------------------
-    def test_set_misplaced_data(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        # with self.assertRaises(ValueError):
-        #    T.set_data(np.ones((5, 5, 5)), offset=(8, 8, 8))
-        self.assertRaises(ValueError, T.set_data,
-                          np.ones((5, 5, 5)), offset=(8, 8, 8))
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+    # with self.assertRaises(ValueError):
+    #    T.set_data(np.ones((5, 5, 5)), offset=(8, 8, 8))
+    assert_raises(ValueError, T.set_data,
+                  np.ones((5, )), offset=(8, ))
 
     # Set misshaped data
     # ---------------------------------
-    def test_set_misshaped_data_3D(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        # with self.assertRaises(ValueError):
-        #    T.set_data(np.ones((10, 10, 10)))
-        self.assertRaises(ValueError, T.set_data, np.ones((10,)))
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+    # with self.assertRaises(ValueError):
+    #    T.set_data(np.ones((10, 10, 10)))
+    assert_raises(ValueError, T.set_data, np.ones((10, 10)))
 
     # Set whole data (clear pending data)
     # ---------------------------------
-    def test_set_whole_data(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        T.set_data(np.ones((10, 10, 10), np.uint8))
-        assert T.shape == (10, 10, 10, 1)
-        assert len(T._pending_data) == 1
-
-    # Test view get invalidated when base is resized
-    # ----------------------------------------------
-    def test_invalid_views(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        Z = T[5:, 5:, 5:]
-        T.resize((5, 5, 5))
-        assert Z._valid is False
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+    T.set_data(np.ones((10, ), np.uint8))
+    assert T.shape == (10, 1)
+    glir_cmd = T._glir.clear()[-1]
+    assert glir_cmd[0] == 'DATA'
 
     # Test set data with different shape
     # ---------------------------------
-    def test_reset_data_shape(self):
-        shape1 = 10, 10, 10
-        shape3 = 10, 10, 10, 3
-        
-        # Init data (explicit shape)
-        data = np.zeros((10, 10, 10, 1), dtype=np.uint8)
-        T = Texture3D(data=data)
-        assert T.shape == (10, 10, 10, 1)
-        assert T._format == gl.GL_LUMINANCE
-        
-        # Set data to rgb
-        T.set_data(np.zeros(shape3, np.uint8))
-        assert T.shape == (10, 10, 10, 3)
-        assert T._format == gl.GL_RGB
-        
-        # Set data to grayscale
-        T.set_data(np.zeros(shape1, np.uint8))
-        assert T.shape == (10, 10, 10, 1)
-        assert T._format == gl.GL_LUMINANCE
+    shape1 = (10, )
+    shape3 = (10, 3)
+
+    # Init data (explicit shape)
+    data = np.zeros((10, 1), dtype=np.uint8)
+    T = Texture1D(data=data)
+    assert T.shape == (10, 1)
+    assert T._format == 'luminance'
+
+    # Set data to rgb
+    T.set_data(np.zeros(shape3, np.uint8))
+    assert T.shape == (10, 3)
+    assert T._format == 'rgb'
+
+    # Set data to grayscale
+    T.set_data(np.zeros(shape1, np.uint8))
+    assert T.shape == (10, 1)
+    assert T._format == 'luminance'
+
+    # Set size to rgb
+    T.resize(shape3)
+    assert T.shape == (10, 3)
+    assert T._format == 'rgb'
+
+    # Set size to grayscale
+    T.resize(shape1)
+    assert T.shape == (10, 1)
+    assert T._format == 'luminance'
+
+    # Test set data with different shape and type
+    # -------------------------------------------
+    data = np.zeros((10, ), dtype=np.uint8)
+    T = Texture1D(data=data)
+
+    data = np.zeros((10, ), dtype=np.float32)
+    T.set_data(data)
+
+    data = np.zeros((12, ), dtype=np.int32)
+    T.set_data(data)
+
+
+# --------------------------------------------------------------- Texture3D ---
+ at requires_pyopengl()
+def test_texture_3D():
+    # Note: put many tests related to (re)sizing here, because Texture
+    # is not really aware of shape.
+
+    # Shape extension
+    # ---------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    assert T._shape == (10, 10, 10, 1)
+    assert 'Texture3D' in repr(T)
+    assert T.glsl_type == ('uniform', 'sampler3D')
         
-        # Set size to rgb
-        T.resize(shape3)
-        assert T.shape == (10, 10, 10, 3)
-        assert T._format == gl.GL_RGB
+    # Width & height
+    # ---------------------------------
+    data = np.zeros((10, 20, 30), dtype=np.uint8)
+    T = Texture3D(data=data)
+    assert T.width == 30
+    assert T.height == 20
+    assert T.depth == 10
+
+    # Resize
+    # ---------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    T.resize((5, 5, 5))
+    assert T.shape == (5, 5, 5, 1)
+    glir_cmd = T._glir.clear()[-1]
+    assert glir_cmd[0] == 'SIZE'
+    assert glir_cmd[2] == (5, 5, 5, 1)
+    
+    # Resize with bad shape
+    # ---------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    # with self.assertRaises(ValueError):
+    #    T.resize((5, 5, 5, 5))
+    assert_raises(ValueError, T.resize, (5, 5, 5, 5))
+
+    # Resize not resizable
+    # ---------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data, resizable=False)
+    # with self.assertRaises(RuntimeError):
+    #    T.resize((5, 5, 5))
+    assert_raises(RuntimeError, T.resize, (5, 5, 5))
+
+    # Set oversized data (-> resize)
+    # ---------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    T.set_data(np.ones((20, 20, 20), np.uint8))
+    assert T.shape == (20, 20, 20, 1)
         
-        # Set size to grayscale
-        T.resize(shape1)
-        assert T.shape == (10, 10, 10, 1)
-        assert T._format == gl.GL_LUMINANCE
+    # Set undersized data
+    # ---------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    T.set_data(np.ones((5, 5, 5), np.uint8))
+    assert T.shape == (5, 5, 5, 1)
+
+    # Set misplaced data
+    # ---------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    # with self.assertRaises(ValueError):
+    #    T.set_data(np.ones((5, 5, 5)), offset=(8, 8, 8))
+    assert_raises(ValueError, T.set_data,
+                  np.ones((5, 5, 5)), offset=(8, 8, 8))
+
+    # Set misshaped data
+    # ---------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    # with self.assertRaises(ValueError):
+    #    T.set_data(np.ones((10, 10, 10)))
+    assert_raises(ValueError, T.set_data, np.ones((10,)))
+
+    # Set whole data (clear pending data)
+    # ---------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    T.set_data(np.ones((10, 10, 10), np.uint8))
+    assert T.shape == (10, 10, 10, 1)
+    glir_cmd = T._glir.clear()[-1]
+    assert glir_cmd[0] == 'DATA'
 
     # Test set data with different shape
     # ---------------------------------
-    def test_reset_data_type(self):
-        shape = 10, 10, 10
-        T = Texture3D(data=np.zeros(shape, dtype=np.uint8))
-        assert T.dtype == np.uint8
-        assert T._gtype == gl.GL_UNSIGNED_BYTE
-        
-        newdata = np.zeros(shape, dtype=np.float32)
-        self.assertRaises(ValueError, T.set_data, newdata)
-        
-        newdata = np.zeros(shape, dtype=np.int32)
-        self.assertRaises(ValueError, T.set_data, newdata)
+    shape1 = 10, 10, 10
+    shape3 = 10, 10, 10, 3
+    
+    # Init data (explicit shape)
+    data = np.zeros((10, 10, 10, 1), dtype=np.uint8)
+    T = Texture3D(data=data)
+    assert T.shape == (10, 10, 10, 1)
+    assert T._format == 'luminance'
+    
+    # Set data to rgb
+    T.set_data(np.zeros(shape3, np.uint8))
+    assert T.shape == (10, 10, 10, 3)
+    assert T._format == 'rgb'
+    
+    # Set data to grayscale
+    T.set_data(np.zeros(shape1, np.uint8))
+    assert T.shape == (10, 10, 10, 1)
+    assert T._format == 'luminance'
+    
+    # Set size to rgb
+    T.resize(shape3)
+    assert T.shape == (10, 10, 10, 3)
+    assert T._format == 'rgb'
+    
+    # Set size to grayscale
+    T.resize(shape1)
+    assert T.shape == (10, 10, 10, 1)
+    assert T._format == 'luminance'
+
+    # Test set data with different shape and type
+    # -------------------------------------------
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    
+    data = np.zeros((10, 11, 11), dtype=np.float32)
+    T.set_data(data)
+    
+    data = np.zeros((12, 12, 10), dtype=np.int32)
+    T.set_data(data)
 
 
-# -----------------------------------------------------------------------------
-if __name__ == "__main__":
-    unittest.main()
+class TextureAtlasTest(unittest.TestCase):
+    
+    def test_init_atas(self):
+        T = TextureAtlas((100, 100))
+        assert T.shape == (128, 128, 3)  # rounds to powers of 2
+        
+        for i in [10, 20, 30, 40, 50, 60]:
+            reg = T.get_free_region(10, 10)
+            assert len(reg) == 4
+        
+        reg = T.get_free_region(129, 129)
+        assert reg is None
+    
+    
+# --------------------------------------------------------- Texture formats ---
+def _test_texture_formats(Texture, baseshape, formats):
+
+    # valid channel count and format combinations
+    for channels in range(1, 5):
+        for format in [f for n, f in formats if n == channels]:
+            shape = baseshape + (channels,)
+            T = Texture(shape=shape, format=format)
+            assert 'Texture' in repr(T)
+            assert T._shape == shape
+            data = np.zeros(shape, dtype=np.uint8)
+            T = Texture(data=data, format=format)
+            assert 'Texture' in repr(T)
+            assert T._shape == shape
+
+    # invalid channel count and format combinations
+    for channels in range(1, 5):
+        for format in [f for n, f in formats + [(5, 'junk')] if n != channels]:
+            shape = baseshape + (channels,)
+            assert_raises(ValueError, Texture, shape=shape, format=format)
+            data = np.zeros(shape, dtype=np.uint8)
+            assert_raises(ValueError, Texture, data=data, format=format)
+
+
+# --------------------------------------------------------- Texture formats ---
+def _test_texture_basic_formats(Texture, baseshape):
+    _test_texture_formats(
+        Texture, 
+        baseshape, 
+        [
+            (1, 'alpha'),
+            (1, 'luminance'),
+            (2, 'luminance_alpha'),
+            (3, 'rgb'),
+            (4, 'rgba')
+        ]
+    )
+
+
+# ------------------------------------------------------- Texture1D formats ---
+def test_texture_1D_formats():
+    _test_texture_basic_formats(Texture1D, (10, ))
+
+
+# ------------------------------------------------------- Texture2D formats ---
+def test_texture_2D_formats():
+    _test_texture_basic_formats(Texture2D, (10, 10))
+
+
+# ------------------------------------------------------- Texture3D formats ---
+def test_texture_3D_formats():
+    _test_texture_basic_formats(Texture3D, (10, 10, 10))
+
+
+# -------------------------------------------------- Texture OpenGL formats ---
+def _test_texture_opengl_formats(Texture, baseshape):
+    _test_texture_formats(
+        Texture,
+        baseshape,
+        [
+            (1, 'red'),
+            (2, 'rg'),
+            (3, 'rgb'),
+            (4, 'rgba')
+        ]
+    )
+
+
+# ------------------------------------------------ Texture1D OpenGL formats ---
+ at requires_pyopengl()
+def test_texture_1D_opengl_formats():
+    _test_texture_opengl_formats(Texture1D, (10, ))
+
+
+# ------------------------------------------------ Texture2D OpenGL formats ---
+ at requires_pyopengl()
+def test_texture_2D_opengl_formats():
+    _test_texture_opengl_formats(Texture2D, (10, 10))
+
+
+# ------------------------------------------------ Texture3D OpenGL formats ---
+ at requires_pyopengl()
+def test_texture_3D_opengl_formats():
+    _test_texture_opengl_formats(Texture3D, (10, 10, 10))
+
+
+# ------------------------------------------ Texture OpenGL internalformats ---
+def _test_texture_internalformats(Texture, baseshape):
+    # Test format for concrete Texture class and baseshape + (numchannels,)
+    # Test internalformats valid with desktop OpenGL
+
+    formats = [
+        (1, 'red', ['red', 'r8', 'r16', 'r16f', 'r32f']),
+        (2, 'rg', ['rg', 'rg8', 'rg16', 'rg16f', 'rg32f']),
+        (3, 'rgb', ['rgb', 'rgb8', 'rgb16', 'rgb16f', 'rgb32f']),
+        (4, 'rgba', ['rgba', 'rgba8', 'rgba16', 'rgba16f', 'rgba32f'])
+    ]
+        
+    for channels in range(1, 5):
+        for fmt, ifmts in [(f, iL) for n, f, iL in formats if n == channels]:
+            shape = baseshape + (channels,)
+            data = np.zeros(shape, dtype=np.uint8)
+            for ifmt in ifmts:
+                T = Texture(shape=shape, format=fmt, internalformat=ifmt)
+                assert 'Texture' in repr(T)
+                assert T._shape == shape
+                T = Texture(data=data, format=fmt, internalformat=ifmt)
+                assert 'Texture' in repr(T)
+                assert T._shape == shape
+
+    for channels in range(1, 5):
+        for fmt, ifmts in [(f, iL) for n, f, iL in formats if n != channels]:
+            shape = baseshape + (channels,)
+            data = np.zeros(shape, dtype=np.uint8)
+            for ifmt in ifmts:
+                assert_raises(ValueError, Texture, shape=shape, format=fmt,
+                              internalformat=ifmt)
+                assert_raises(ValueError, Texture, data=data, format=fmt,
+                              internalformat=ifmt)
+
+
+# ---------------------------------------- Texture2D OpenGL internalformats ---
+ at requires_pyopengl()
+def test_texture_2D_internalformats():
+    _test_texture_internalformats(Texture2D, (10, 10))
+
+
+# ---------------------------------------- Texture3D OpenGL internalformats ---
+ at requires_pyopengl()
+def test_texture_3D_internalformats():
+    _test_texture_internalformats(Texture3D, (10, 10, 10))
+
+
+run_tests_if_main()
diff --git a/vispy/gloo/tests/test_use_gloo.py b/vispy/gloo/tests/test_use_gloo.py
index 853aa92..2a73db0 100644
--- a/vispy/gloo/tests/test_use_gloo.py
+++ b/vispy/gloo/tests/test_use_gloo.py
@@ -5,13 +5,14 @@
 # -----------------------------------------------------------------------------
 import numpy as np
 from numpy.testing import assert_allclose
-from nose.tools import assert_raises, assert_equal
 
 from vispy.app import Canvas
 from vispy.gloo import (Texture2D, Texture3D, Program, FrameBuffer,
-                        ColorBuffer, DepthBuffer, set_viewport, clear)
+                        RenderBuffer, set_viewport, clear)
 from vispy.gloo.util import draw_texture, _screenshot
-from vispy.testing import requires_application, has_pyopengl
+from vispy.testing import (requires_application, has_pyopengl,
+                           run_tests_if_main,
+                           assert_raises, assert_equal)
 
 
 @requires_application()
@@ -24,24 +25,27 @@ def test_use_textures():
 @requires_application()
 def test_use_framebuffer():
     """Test drawing to a framebuffer"""
-    shape = (100, 100)
+    shape = (100, 300)  # for some reason Windows wants a tall window...
     data = np.random.rand(*shape).astype(np.float32)
-    orig_tex = Texture2D(data)
     use_shape = shape + (3,)
-    fbo_tex = Texture2D(shape=use_shape, dtype=np.ubyte, format='rgb')
-    rbo = ColorBuffer(shape=shape)
-    fbo = FrameBuffer(color=fbo_tex)
-    with Canvas(size=(100, 100)) as c:
+    with Canvas(size=shape[::-1]) as c:
+        orig_tex = Texture2D(data)
+        fbo_tex = Texture2D(use_shape, format='rgb')
+        rbo = RenderBuffer(shape, 'color')
+        fbo = FrameBuffer(color=fbo_tex)
+        c.context.glir.set_verbose(True)
+        assert_equal(c.size, shape[::-1])
         set_viewport((0, 0) + c.size)
         with fbo:
             draw_texture(orig_tex)
         draw_texture(fbo_tex)
         out_tex = _screenshot()[::-1, :, 0].astype(np.float32)
+        assert_equal(out_tex.shape, c.size[::-1])
         assert_raises(TypeError, FrameBuffer.color_buffer.fset, fbo, 1.)
         assert_raises(TypeError, FrameBuffer.depth_buffer.fset, fbo, 1.)
         assert_raises(TypeError, FrameBuffer.stencil_buffer.fset, fbo, 1.)
         fbo.color_buffer = rbo
-        fbo.depth_buffer = DepthBuffer(shape)
+        fbo.depth_buffer = RenderBuffer(shape)
         fbo.stencil_buffer = None
         print((fbo.color_buffer, fbo.depth_buffer, fbo.stencil_buffer))
         clear(color='black')
@@ -59,9 +63,6 @@ def test_use_texture3D():
     vals = [0, 200, 100, 0, 255, 0, 100]
     d, h, w = len(vals), 3, 5
     data = np.zeros((d, h, w), np.float32)
-    if not has_pyopengl():
-        assert_raises(ImportError, Texture3D(data))
-        return
 
     VERT_SHADER = """
     attribute vec2 a_pos;
@@ -88,15 +89,18 @@ def test_use_texture3D():
     # populate the depth "slices" with different gray colors in the bottom left
     for ii, val in enumerate(vals):
         data[ii, :2, :3] = val / 255.
-    program = Program(VERT_SHADER, FRAG_SHADER)
-    program['a_pos'] = [[-1., -1.], [1., -1.], [-1., 1.], [1., 1.]]
-    tex = Texture3D(data)
-    assert_equal(tex.width, w)
-    assert_equal(tex.height, h)
-    assert_equal(tex.depth, d)
-    tex.interpolation = 'nearest'
-    program['u_texture'] = tex
-    with Canvas(size=(100, 100)):
+    with Canvas(size=(100, 100)) as c:
+        if not has_pyopengl():
+            t = Texture3D(data)
+            assert_raises(ImportError, t.glir.flush, c.context.shared.parser)
+            return
+        program = Program(VERT_SHADER, FRAG_SHADER)
+        program['a_pos'] = [[-1., -1.], [1., -1.], [-1., 1.], [1., 1.]]
+        tex = Texture3D(data, interpolation='nearest')
+        assert_equal(tex.width, w)
+        assert_equal(tex.height, h)
+        assert_equal(tex.depth, d)
+        program['u_texture'] = tex
         for ii, val in enumerate(vals):
             set_viewport(0, 0, w, h)
             clear(color='black')
@@ -108,3 +112,62 @@ def test_use_texture3D():
             expected = np.zeros_like(out)
             expected[:2, :3] = val
             assert_allclose(out, expected, atol=1./255.)
+
+
+ at requires_application()
+def test_use_uniforms():
+    """Test using uniform arrays"""
+    VERT_SHADER = """
+    attribute vec2 a_pos;
+    varying vec2 v_pos;
+
+    void main (void)
+    {
+        v_pos = a_pos;
+        gl_Position = vec4(a_pos, 0., 1.);
+    }
+    """
+
+    FRAG_SHADER = """
+    varying vec2 v_pos;
+    uniform vec3 u_color[2];
+
+    void main()
+    {
+        gl_FragColor = vec4((u_color[0] + u_color[1]) / 2., 1.);
+    }
+    """
+    shape = (300, 300)
+    with Canvas(size=shape) as c:
+        c.context.glir.set_verbose(True)
+        assert_equal(c.size, shape[::-1])
+        shape = (3, 3)
+        set_viewport((0, 0) + shape)
+        program = Program(VERT_SHADER, FRAG_SHADER)
+        program['a_pos'] = [[-1., -1.], [1., -1.], [-1., 1.], [1., 1.]]
+        program['u_color'] = np.ones((2, 3))
+        c.context.clear('k')
+        program.draw('triangle_strip')
+        out = _screenshot()
+        assert_allclose(out[:, :, 0] / 255., np.ones(shape), atol=1. / 255.)
+
+        # now set one element
+        program['u_color[1]'] = np.zeros(3, np.float32)
+        c.context.clear('k')
+        program.draw('triangle_strip')
+        out = _screenshot()
+        assert_allclose(out[:, :, 0] / 255., 127.5 / 255. * np.ones(shape),
+                        atol=1. / 255.)
+
+        # and the other
+        assert_raises(ValueError, program.__setitem__, 'u_color',
+                      np.zeros(3, np.float32))
+        program['u_color'] = np.zeros((2, 3), np.float32)
+        program['u_color[0]'] = np.ones(3, np.float32)
+        c.context.clear((0.33,) * 3)
+        program.draw('triangle_strip')
+        out = _screenshot()
+        assert_allclose(out[:, :, 0] / 255., 127.5 / 255. * np.ones(shape),
+                        atol=1. / 255.)
+
+run_tests_if_main()
diff --git a/vispy/gloo/tests/test_util.py b/vispy/gloo/tests/test_util.py
new file mode 100644
index 0000000..ff8d892
--- /dev/null
+++ b/vispy/gloo/tests/test_util.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+
+from vispy.gloo import util
+
+from vispy.testing import run_tests_if_main, assert_raises
+
+
+def test_check_enum():
+    from vispy.gloo import gl
+    
+    # Test enums
+    assert util.check_enum(gl.GL_RGB) == 'rgb' 
+    assert util.check_enum(gl.GL_TRIANGLE_STRIP) == 'triangle_strip' 
+    
+    # Test strings
+    assert util.check_enum('RGB') == 'rgb' 
+    assert util.check_enum('Triangle_STRIp') == 'triangle_strip' 
+    
+    # Test wrong input
+    assert_raises(ValueError, util.check_enum, int(gl.GL_RGB))
+    assert_raises(ValueError, util.check_enum, int(gl.GL_TRIANGLE_STRIP))
+    assert_raises(ValueError, util.check_enum, [])
+    
+    # Test with test
+    util.check_enum('RGB', 'test', ('rgb', 'alpha')) == 'rgb' 
+    util.check_enum(gl.GL_ALPHA, 'test', ('rgb', 'alpha')) == 'alpha' 
+    #
+    assert_raises(ValueError, util.check_enum, 'RGB', 'test', ('a', 'b'))
+    assert_raises(ValueError, util.check_enum, gl.GL_ALPHA, 'test', ('a', 'b'))
+    
+    # Test PyOpenGL enums
+    try:
+        from OpenGL import GL
+    except ImportError:
+        return  # we cannot test PyOpenGL
+    #
+    assert util.check_enum(GL.GL_RGB) == 'rgb' 
+    assert util.check_enum(GL.GL_TRIANGLE_STRIP) == 'triangle_strip' 
+
+
+def test_check_identifier():
+    
+    # Tests check_identifier()
+    assert util.check_identifier('foo') is None
+    assert util.check_identifier('_fooBarBla') is None
+    assert util.check_identifier('_glPosition') is None
+    
+    # Wrong identifier
+    assert util.check_identifier('__').startswith('Identifier')
+    assert util.check_identifier('gl_').startswith('Identifier')
+    assert util.check_identifier('GL_').startswith('Identifier')
+    assert util.check_identifier('double').startswith('Identifier')
+        
+    # Test check_variable()
+    assert util.check_variable('foo') is None
+    assert util.check_variable('a' * 30) is None
+    assert util.check_variable('a' * 32)
+
+    
+run_tests_if_main()
diff --git a/vispy/gloo/tests/test_variable.py b/vispy/gloo/tests/test_variable.py
deleted file mode 100644
index bca74cd..0000000
--- a/vispy/gloo/tests/test_variable.py
+++ /dev/null
@@ -1,139 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
-# Distributed under the terms of the new BSD License.
-# -----------------------------------------------------------------------------
-import unittest
-import numpy as np
-
-from vispy.gloo import gl
-from vispy.gloo.variable import Uniform, Variable, Attribute
-
-
-# -----------------------------------------------------------------------------
-class VariableTest(unittest.TestCase):
-
-    def test_init(self):
-        variable = Variable(None, "A", gl.GL_FLOAT)
-        assert variable._handle == -1
-        assert variable.name == "A"
-        assert variable.data is None
-        assert variable.gtype == gl.GL_FLOAT
-        assert variable.enabled is True
-
-    def test_init_wrong_type(self):
-        # with self.assertRaises(TypeError):
-        #    v = Variable(None, "A", gl.GL_INT_VEC2)
-        self.assertRaises(TypeError, Variable, None, "A", gl.GL_INT_VEC2)
-
-        # with self.assertRaises(TypeError):
-        #    v = Variable(None, "A", gl.GL_INT_VEC3)
-        self.assertRaises(TypeError, Variable, None, "A", gl.GL_INT_VEC3)
-
-        # with self.assertRaises(TypeError):
-        #    v = Variable(None, "A", gl.GL_INT_VEC4)
-        self.assertRaises(TypeError, Variable, None, "A", gl.GL_INT_VEC4)
-
-        # with self.assertRaises(TypeError):
-        #    v = Variable(None, "A", gl.GL_BOOL_VEC2)
-        self.assertRaises(TypeError, Variable, None, "A", gl.GL_BOOL_VEC2)
-
-        # with self.assertRaises(TypeError):
-        #    v = Variable(None, "A", gl.GL_BOOL_VEC3)
-        self.assertRaises(TypeError, Variable, None, "A", gl.GL_BOOL_VEC3)
-
-        # with self.assertRaises(TypeError):
-        #    v = Variable(None, "A", gl.GL_BOOL_VEC4)
-        self.assertRaises(TypeError, Variable, None, "A", gl.GL_BOOL_VEC4)
-
-
-# -----------------------------------------------------------------------------
-class UniformTest(unittest.TestCase):
-
-    def test_init(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT)
-        assert uniform._unit == -1
-
-    def test_float(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT)
-        assert uniform.data.dtype == np.float32
-        assert uniform.data.size == 1
-
-    def test_vec2(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT_VEC2)
-        assert uniform.data.dtype == np.float32
-        assert uniform.data.size == 2
-
-    def test_vec3(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT_VEC2)
-        assert uniform.data.dtype == np.float32
-        assert uniform.data.size == 2
-
-    def test_vec4(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT_VEC2)
-        assert uniform.data.dtype == np.float32
-        assert uniform.data.size == 2
-
-    def test_int(self):
-        uniform = Uniform(None, "A", gl.GL_INT)
-        assert uniform.data.dtype == np.int32
-        assert uniform.data.size == 1
-
-    def test_mat2(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT_MAT2)
-        assert uniform.data.dtype == np.float32
-        assert uniform.data.size == 4
-
-    def test_mat3(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT_MAT3)
-        assert uniform.data.dtype == np.float32
-        assert uniform.data.size == 9
-
-    def test_mat4(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT_MAT4)
-        assert uniform.data.dtype == np.float32
-        assert uniform.data.size == 16
-
-    def test_set(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT_VEC4)
-
-        uniform.set_data(1)
-        assert (uniform.data == 1).all()
-
-        uniform.set_data([1, 2, 3, 4])
-        assert (uniform.data == [1, 2, 3, 4]).all()
-
-    def test_set_exception(self):
-        uniform = Uniform(None, "A", gl.GL_FLOAT_VEC4)
-
-        # with self.assertRaises(ValueError):
-        #    uniform.set_data([1, 2])
-        self.assertRaises(ValueError, uniform.set_data, [1, 2])
-
-        # with self.assertRaises(ValueError):
-        #    uniform.set_data([1, 2, 3, 4, 5])
-        self.assertRaises(ValueError, uniform.set_data, [1, 2, 3, 4, 5])
-
-
-# -----------------------------------------------------------------------------
-class AttributeTest(unittest.TestCase):
-
-    def test_init(self):
-        attribute = Attribute(None, "A", gl.GL_FLOAT)
-        assert attribute.size == 0
-
-    def test_set_generic(self):
-        attribute = Attribute(None, "A", gl.GL_FLOAT_VEC4)
-
-        attribute.set_data(1)
-        assert type(attribute.data) is np.ndarray
-
-#    @unittest.expectedFailure
-#    def test_set_generic_2(self):
-#        attribute = Attribute(None, "A", gl.GL_FLOAT_VEC4)
-#        attribute.set_data([1, 2, 3, 4])
-#        assert type(attribute.data) is np.ndarray
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/vispy/gloo/tests/test_wrappers.py b/vispy/gloo/tests/test_wrappers.py
index 8eaa186..73cca5a 100644
--- a/vispy/gloo/tests/test_wrappers.py
+++ b/vispy/gloo/tests/test_wrappers.py
@@ -4,37 +4,177 @@
 # Distributed under the terms of the new BSD License.
 # -----------------------------------------------------------------------------
 import numpy as np
-from numpy.testing import assert_array_equal
-from nose.tools import assert_true, assert_equal, assert_raises
+from numpy.testing import assert_array_equal, assert_allclose
 
 from vispy import gloo
 from vispy.gloo import gl
 from vispy.app import Canvas
-from vispy.testing import requires_application
+from vispy.testing import (requires_application, run_tests_if_main,
+                           assert_true, assert_equal, assert_raises)
 from vispy.gloo import read_pixels
+from vispy.gloo.glir import GlirQueue
+from vispy.gloo import wrappers
+
+
+# Dummy queue
+dummy_glir = GlirQueue()
+dummy_glir.context = dummy_glir
+dummy_glir.glir = dummy_glir
+
+
+def install_dummy_glir():
+    wrappers.get_current_canvas = lambda x=None: dummy_glir
+    dummy_glir.clear()
+    return dummy_glir
+
+
+def reset_glir():
+    wrappers.get_current_canvas = gloo.get_current_canvas
+
+
+def teardown_module():
+    reset_glir()
+
+
+def test_wrappers_basic_glir():
+    """ Test that basic gloo wrapper functions emit right GLIR command """
+    
+    glir = install_dummy_glir()
+    
+    funcs = [('viewport', 0, 0, 10, 10),
+             ('depth_range', 0, 1),
+             ('front_face', 'ccw'),
+             ('cull_face', 'back'),
+             ('line_width', 1),
+             ('polygon_offset', 0, 0),
+             ('clear_color', ),
+             ('clear_depth', ),
+             ('clear_stencil', ),
+             ('blend_func', ),
+             ('blend_color', 'red'),
+             ('blend_equation', 'X'),
+             ('scissor', 0, 0, 10, 10),
+             ('stencil_func', ),
+             ('stencil_mask', ),
+             ('stencil_op', ),
+             ('depth_func', ),
+             ('depth_mask', 'X'),
+             ('color_mask', False, False, False, False),
+             ('sample_coverage', ),
+             ('hint', 'foo', 'bar'),
+             # not finish and flush, because that would flush the glir queue
+             ]
+    
+    for func in funcs:
+        name, args = func[0], func[1:]
+        f = getattr(gloo, 'set_' + name)
+        f(*args)
+    
+    cmds = glir.clear()
+    assert len(cmds) == len(funcs)
+    for i, func in enumerate(funcs):
+        cmd = cmds[i]
+        nameparts = [a.capitalize() for a in func[0].split('_')]
+        name = 'gl' + ''.join(nameparts)
+        assert cmd[0] == 'FUNC'
+        if cmd[1].endswith('Separate'):
+            assert cmd[1][:-8] == name
+        else:
+            assert cmd[1] == name
+    
+    reset_glir()
+
+
+def test_wrappers_glir():
+    """ Test that special wrapper functions do what they must do """
+
+    glir = install_dummy_glir()
+    
+    # Test clear() function
+    gloo.clear()
+    cmds = glir.clear()
+    assert len(cmds) == 1
+    assert cmds[0][0] == 'FUNC'
+    assert cmds[0][1] == 'glClear'
+    #
+    gloo.clear(True, False, False)
+    cmds = glir.clear()
+    assert len(cmds) == 1
+    assert cmds[0][0] == 'FUNC'
+    assert cmds[0][1] == 'glClear'
+    assert cmds[0][2] == gl.GL_COLOR_BUFFER_BIT
+    #
+    gloo.clear('red')
+    cmds = glir.clear()
+    assert len(cmds) == 2
+    assert cmds[0][0] == 'FUNC'
+    assert cmds[0][1] == 'glClearColor'
+    assert cmds[1][0] == 'FUNC'
+    assert cmds[1][1] == 'glClear'
+    #
+    gloo.clear('red', 4, 3)
+    cmds = glir.clear()
+    assert len(cmds) == 4
+    assert cmds[0][1] == 'glClearColor'
+    assert cmds[1][1] == 'glClearDepth'
+    assert cmds[2][1] == 'glClearStencil'
+    assert cmds[3][1] == 'glClear'
+    
+    # Test set_state() function
+    gloo.set_state(foo=True, bar=False)
+    cmds = set(glir.clear())
+    assert len(cmds) == 2
+    assert ('FUNC', 'glEnable', 'foo') in cmds
+    assert ('FUNC', 'glDisable', 'bar') in cmds
+    #
+    gloo.set_state(viewport=(0, 0, 10, 10), clear_color='red')
+    cmds = sorted(glir.clear())
+    assert len(cmds) == 2
+    assert cmds[0][1] == 'glClearColor'
+    assert cmds[1][1] == 'glViewport'
+    #
+    presets = gloo.get_state_presets()
+    a_preset = list(presets.keys())[0]
+    gloo.set_state(a_preset)
+    cmds = sorted(glir.clear())
+    assert len(cmds) == len(presets[a_preset])
+    
+    reset_glir()
+
+
+def assert_cmd_raises(E, fun, *args, **kwargs):
+    gloo.flush()  # no error here
+    fun(*args, **kwargs)
+    assert_raises(E, gloo.flush)
 
 
 @requires_application()
 def test_wrappers():
     """Test gloo wrappers"""
     with Canvas():
-        gl.use_gl('desktop debug')
+        gl.use_gl('gl2 debug')
+        gloo.clear('#112233')  # make it so that there's something non-zero
         # check presets
         assert_raises(ValueError, gloo.set_state, preset='foo')
         for state in gloo.get_state_presets().keys():
             gloo.set_state(state)
         assert_raises(ValueError, gloo.set_blend_color, (0., 0.))  # bad color
         assert_raises(TypeError, gloo.set_hint, 1, 2)  # need strs
-        assert_raises(TypeError, gloo.get_parameter, 1)  # need str
         # this doesn't exist in ES 2.0 namespace
-        assert_raises(ValueError, gloo.set_hint, 'fog_hint', 'nicest')
+        assert_cmd_raises(ValueError, gloo.set_hint, 'fog_hint', 'nicest')
         # test bad enum
         assert_raises(RuntimeError, gloo.set_line_width, -1)
 
         # check read_pixels
-        assert_true(isinstance(gloo.read_pixels(), np.ndarray))
+        x = gloo.read_pixels()
+        assert_true(isinstance(x, np.ndarray))
         assert_true(isinstance(gloo.read_pixels((0, 0, 1, 1)), np.ndarray))
         assert_raises(ValueError, gloo.read_pixels, (0, 0, 1))  # bad port
+        y = gloo.read_pixels(alpha=False, out_type=np.ubyte)
+        assert_equal(y.shape, x.shape[:2] + (3,))
+        assert_array_equal(x[..., :3], y)
+        y = gloo.read_pixels(out_type='float')
+        assert_allclose(x/255., y)
 
         # now let's (indirectly) check our set_* functions
         viewport = (0, 0, 1, 1)
@@ -62,9 +202,9 @@ def test_wrappers():
         gloo.flush()
         gloo.finish()
         # check some results
-        assert_array_equal(gloo.get_parameter('viewport'), viewport)
-        assert_equal(gloo.get_parameter('front_face'), gl.GL_CW)
-        assert_equal(gloo.get_parameter('blend_color'), blend_color + (1,))
+        assert_array_equal(gl.glGetParameter(gl.GL_VIEWPORT), viewport)
+        assert_equal(gl.glGetParameter(gl.GL_FRONT_FACE), gl.GL_CW)
+        assert_equal(gl.glGetParameter(gl.GL_BLEND_COLOR), blend_color + (1,))
 
 
 @requires_application()
@@ -89,18 +229,22 @@ def test_read_pixels():
     """
 
     with Canvas() as c:
+        gloo.set_viewport(0, 0, *c.size)
         c._program = gloo.Program(VERT_SHADER, FRAG_SHADER)
         c._program['a_position'] = gloo.VertexBuffer(vPosition)
-        gloo.set_clear_color((0, 0, 0, 0))  # Black background
-        gloo.clear()
+        gloo.clear(color='black')
         c._program.draw('triangle_strip')
 
         # Check if the return of read_pixels is the same as our drawing
-        img = read_pixels()
-        top_left = sum(img[0][0])
+        img = read_pixels(alpha=False)
+        assert_equal(img.shape[:2], c.size[::-1])
+        top_left = sum(img[0, 0])
         assert_true(top_left > 0)  # Should be > 0 (255*4)
         # Sum of the pixels in top right + bottom left + bottom right corners
-        corners = sum(img[0][-1] + img[-1][0] + img[-1][-1])
+        corners = sum(img[0, -1] + img[-1, 0] + img[-1, -1])
         assert_true(corners == 0)  # Should be all 0
         gloo.flush()
         gloo.finish()
+
+
+run_tests_if_main()
diff --git a/vispy/gloo/texture.py b/vispy/gloo/texture.py
index 2fcc08c..6669b5b 100644
--- a/vispy/gloo/texture.py
+++ b/vispy/gloo/texture.py
@@ -1,54 +1,17 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
-import numpy as np
-
-from . import gl
-from .globject import GLObject
-from .wrappers import _check_conversion
-from ..util import logger
-
-
-GL_SAMPLER_3D = 35679
-
-
-def _check_pyopengl_3D():
-    """Helper to ensure users have OpenGL for 3D texture support (for now)"""
-    try:
-        import OpenGL.GL as _gl
-    except ImportError:
-        raise ImportError('PyOpenGL is required for 3D texture support')
-    return _gl
 
+import math
 
-def glTexImage3D(target, level, internalformat, format, type, pixels):
-    # Import from PyOpenGL
-    _gl = _check_pyopengl_3D()
-    border = 0
-    assert isinstance(pixels, (tuple, list))  # the only way we use this now
-    depth, height, width = pixels
-    _gl.glTexImage3D(target, level, internalformat,
-                     width, height, depth, border, format, type, None)
-
-
-def glTexSubImage3D(target, level, xoffset, yoffset, zoffset,
-                    format, type, pixels):
-    # Import from PyOpenGL
-    _gl = _check_pyopengl_3D()
-    depth, height, width = pixels.shape[:3]
-    _gl.glTexSubImage3D(target, level, xoffset, yoffset, zoffset,
-                        width, height, depth, format, type, pixels)
-
+import numpy as np
+import warnings
 
-def _check_value(value, valid_dict):
-    """Helper for checking interpolation and wrapping"""
-    if not isinstance(value, (tuple, list)):
-        value = [value] * 2
-    if len(value) != 2:
-        raise ValueError('value must be a single value, or a 2-element list')
-    return tuple(_check_conversion(v, valid_dict) for v in value)
+from .globject import GLObject
+from ..ext.six import string_types
+from .util import check_enum
 
 
 # ----------------------------------------------------------- Texture class ---
@@ -58,156 +21,101 @@ class BaseTexture(GLObject):
 
     Parameters
     ----------
-
-    target : GLEnum
-        gl.GL_TEXTURE2D
-        gl.GL_TEXTURE_CUBE_MAP
-    data : ndarray
-        Texture data (optional)
-    shape : tuple of integers
-        Texture shape (optional)
-    dtype : dtype
-        Texture data type (optional)
-    base : Texture
-        Base texture of this texture
-    offset : tuple of integers
-        Offset of this texture relative to base texture
-    store : bool
-        Specify whether this object stores a reference to the data,
-        allowing the data to be updated regardless of striding. Note
-        that modifying the data after passing it here might result in
-        undesired behavior, unless a copy is given. Default True.
-    resizeable : bool
-        Indicates whether texture can be resized
-    format : str | ENUM
-        The format of the texture: 'luminance', 'alpha', 'luminance_alpha',
-        'rgb', or 'rgba' (or ENUMs GL_LUMINANCE, ALPHA, GL_LUMINANCE_ALPHA,
-        or GL_RGB, GL_RGBA). If not given the format is chosen automatically
-        based on the number of channels. When the data has one channel,
-        'luminance' is assumed.
+    data : ndarray | tuple | None
+        Texture data in the form of a numpy array (or something that
+        can be turned into one). A tuple with the shape of the texture
+        can also be given.
+    format : str | enum | None
+        The format of the texture: 'luminance', 'alpha',
+        'luminance_alpha', 'rgb', or 'rgba'. If not given the format
+        is chosen automatically based on the number of channels.
+        When the data has one channel, 'luminance' is assumed.
+    resizable : bool
+        Indicates whether texture can be resized. Default True.
+    interpolation : str | None
+        Interpolation mode, must be one of: 'nearest', 'linear'.
+        Default 'nearest'.
+    wrapping : str | None
+        Wrapping mode, must be one of: 'repeat', 'clamp_to_edge',
+        'mirrored_repeat'. Default 'clamp_to_edge'.
+    shape : tuple | None
+        Optional. A tuple with the shape of the texture. If ``data``
+        is also a tuple, it will override the value of ``shape``.
+    internalformat : str | None
+        Internal format to use.
+    resizeable : None
+        Deprecated version of `resizable`.
     """
     _ndim = 2
 
     _formats = {
-        1: gl.GL_LUMINANCE,  # or ALPHA,
-        2: gl.GL_LUMINANCE_ALPHA,
-        3: gl.GL_RGB,
-        4: gl.GL_RGBA
+        1: 'luminance',  # or alpha, or red
+        2: 'luminance_alpha',  # or rg
+        3: 'rgb',
+        4: 'rgba'
     }
 
     _inv_formats = {
-        gl.GL_LUMINANCE: 1,
-        gl.GL_ALPHA: 1,
-        gl.GL_LUMINANCE_ALPHA: 2,
-        gl.GL_RGB: 3,
-        gl.GL_RGBA: 4
-    }
-
-    _types = {
-        np.dtype(np.int8): gl.GL_BYTE,
-        np.dtype(np.uint8): gl.GL_UNSIGNED_BYTE,
-        np.dtype(np.int16): gl.GL_SHORT,
-        np.dtype(np.uint16): gl.GL_UNSIGNED_SHORT,
-        np.dtype(np.int32): gl.GL_INT,
-        np.dtype(np.uint32): gl.GL_UNSIGNED_INT,
-        # np.dtype(np.float16) : gl.GL_HALF_FLOAT,
-        np.dtype(np.float32): gl.GL_FLOAT,
-        # np.dtype(np.float64) : gl.GL_DOUBLE
+        'luminance': 1,
+        'alpha': 1,
+        'red': 1,
+        'luminance_alpha': 2,
+        'rg': 2,
+        'rgb': 3,
+        'rgba': 4
     }
 
-    def __init__(self, data=None, shape=None, dtype=None, base=None,
-                 target=None, offset=None, store=True, resizeable=True,
-                 format=None):
+    _inv_internalformats = dict([
+        (base + suffix, channels)
+        for base, channels in [('r', 1), ('rg', 2), ('rgb', 3), ('rgba', 4)]
+        for suffix in ['8', '16', '16f', '32f']
+    ] + [
+        ('luminance', 1),
+        ('alpha', 1),
+        ('red', 1),
+        ('luminance_alpha', 2),
+        ('rg', 2),
+        ('rgb', 3),
+        ('rgba', 4)
+    ])
+
+    def __init__(self, data=None, format=None, resizable=True,
+                 interpolation=None, wrapping=None, shape=None,
+                 internalformat=None, resizeable=None):
         GLObject.__init__(self)
-        self._data = None
-        self._base = base
-        self._store = store
-        self._copy = False  # flag to indicate that a copy is made
-        self._target = target
-        self._offset = offset
-        self._pending_data = []
-        self._resizeable = resizeable
-        self._valid = True
-        self._views = []
-
-        # Extra stages that are handled in _activate()
-        self._need_resize = False
-        self._need_parameterization = True
-        if base is None:
-            self.interpolation = 'nearest'
-            self.wrapping = 'clamp_to_edge'
-
-        # Do we have data to build texture upon ?
+        if resizeable is not None:
+            resizable = resizeable
+            warnings.warn('resizeable has been deprecated in favor of '
+                          'resizable and will be removed next release',
+                          DeprecationWarning)
+
+        # Init shape and format
+        self._resizable = True  # at least while we're in init
+        self._shape = tuple([0 for i in range(self._ndim+1)])
+        self._format = format
+        self._internalformat = internalformat
+
+        # Set texture parameters (before setting data)
+        self.interpolation = interpolation or 'nearest'
+        self.wrapping = wrapping or 'clamp_to_edge'
+
+        # Set data or shape (shape arg is for backward compat)
+        if isinstance(data, tuple):
+            shape, data = data, None
         if data is not None:
-            self._need_resize = True
-            # Handle dtype
-            if dtype is not None:
-                data = np.array(data, dtype=dtype, copy=False)
-            else:
-                data = np.array(data, copy=False)
-            self._dtype = data.dtype
-            # Handle shape
-            data = self._normalize_shape(data)
-            if shape is not None:
-                raise ValueError('Texture needs data or shape, nor both.')
-            self._shape = data.shape
-            # Handle storage
-            if self._store:
-                if not data.flags["C_CONTIGUOUS"]:
-                    logger.warning("Copying discontiguous data as CPU storage")
-                    self._copy = True
-                    data = data.copy()
-                self._data = data
-            # Set data
-            self.set_data(data, copy=False)
-        elif dtype is not None:
             if shape is not None:
-                self._need_resize = True
-            shape = shape or ()
-            self._shape = self._normalize_shape(shape)
-            self._dtype = dtype
-            if self._store:
-                self._data = np.zeros(self._shape, dtype=self._dtype)
+                raise ValueError('Texture needs data or shape, not both.')
+            data = np.array(data, copy=False)
+            # So we can test the combination
+            self._resize(data.shape, format, internalformat)
+            self._set_data(data)
+        elif shape is not None:
+            self._resize(shape, format, internalformat)
         else:
-            raise ValueError("Either data or dtype must be given")
+            raise ValueError("Either data or shape must be given")
 
-        if offset is None:
-            self._offset = (0,) * len(self._shape)
-        else:
-            self._offset = offset
-
-        # Check dtype
-        if hasattr(self._dtype, 'fields') and self._dtype.fields:
-            raise ValueError("Texture dtype cannot be structured")
-
-        self._gtype = BaseTexture._types.get(np.dtype(self.dtype), None)
-        if self._gtype is None:
-            raise ValueError("Type not allowed for texture")
-
-        # Get and check format
-        valid_dict = {'luminance': gl.GL_LUMINANCE,
-                      'alpha': gl.GL_ALPHA,
-                      'luminance_alpha': gl.GL_LUMINANCE_ALPHA,
-                      'rgb': gl.GL_RGB,
-                      'rgba': gl.GL_RGBA}
-        counts = BaseTexture._inv_formats
-        if format is None:
-            if len(self.shape) == 0:
-                raise ValueError('format must be provided if data and shape '
-                                 'are both None')
-            format = BaseTexture._formats.get(self.shape[-1], None)
-            if format is None:
-                raise ValueError("Cannot convert data to texture")
-            self._format = format
-        else:
-            # check to make sure it's a valid entry
-            out_format = _check_conversion(format, valid_dict)
-            # check to make sure that our shape does not conflict with the type
-            if len(self.shape) > 0 and self.shape[-1] != counts[out_format]:
-                raise ValueError('format %s size %s mismatch with input shape '
-                                 '%s' % (format, counts[out_format],
-                                         self.shape[-1]))
-            self._format = out_format
+        # Set resizable (at end of init)
+        self._resizable = bool(resizable)
 
     def _normalize_shape(self, data_or_shape):
         # Get data and shape from input
@@ -234,189 +142,194 @@ class BaseTexture(GLObject):
 
     @property
     def shape(self):
-        """ Texture shape """
+        """ Data shape (last dimension indicates number of color channels)
+        """
         return self._shape
 
     @property
-    def offset(self):
-        """ Texture offset """
-        return self._offset
-
-    @property
-    def dtype(self):
-        """ Texture data type """
-        return self._dtype
-
-    @property
-    def base(self):
-        """ Texture base if this texture is a view on another texture """
-        return self._base
-
-    @property
-    def data(self):
-        """ Texture CPU storage """
-        return self._data
+    def format(self):
+        """ The texture format (color channels).
+        """
+        return self._format
 
     @property
     def wrapping(self):
         """ Texture wrapping mode """
-        if self.base is not None:
-            return self.base.wrapping
         value = self._wrapping
-        return value[0] if value[0] == value[1] else value
+        return value[0] if all([v == value[0] for v in value]) else value
 
     @wrapping.setter
     def wrapping(self, value):
-        """ Texture wrapping mode """
-        if self.base is not None:
-            raise ValueError("Cannot set wrapping on texture view")
-        valid_dict = {'repeat': gl.GL_REPEAT,
-                      'clamp_to_edge': gl.GL_CLAMP_TO_EDGE,
-                      'mirrored_repeat': gl.GL_MIRRORED_REPEAT}
-        self._wrapping = _check_value(value, valid_dict)
-        self._need_parameterization = True
+        # Convert
+        if isinstance(value, int) or isinstance(value, string_types):
+            value = (value,) * self._ndim
+        elif isinstance(value, (tuple, list)):
+            if len(value) != self._ndim:
+                raise ValueError('Texture wrapping needs 1 or %i values' %
+                                 self._ndim)
+        else:
+            raise ValueError('Invalid value for wrapping: %r' % value)
+        # Check and set
+        valid = 'repeat', 'clamp_to_edge', 'mirrored_repeat'
+        value = tuple([check_enum(value[i], 'tex wrapping', valid)
+                       for i in range(self._ndim)])
+        self._wrapping = value
+        self._glir.command('WRAPPING', self._id, value)
 
     @property
     def interpolation(self):
         """ Texture interpolation for minification and magnification. """
-        if self.base is not None:
-            return self.base.interpolation
         value = self._interpolation
         return value[0] if value[0] == value[1] else value
 
     @interpolation.setter
     def interpolation(self, value):
-        """ Texture interpolation for minication and magnification. """
-        if self.base is not None:
-            raise ValueError("Cannot set interpolation on texture view")
-        valid_dict = {'nearest': gl.GL_NEAREST,
-                      'linear': gl.GL_LINEAR}
-        self._interpolation = _check_value(value, valid_dict)
-        self._need_parameterization = True
+        # Convert
+        if isinstance(value, int) or isinstance(value, string_types):
+            value = (value,) * 2
+        elif isinstance(value, (tuple, list)):
+            if len(value) != 2:
+                raise ValueError('Texture interpolation needs 1 or 2 values')
+        else:
+            raise ValueError('Invalid value for interpolation: %r' % value)
+        # Check and set
+        valid = 'nearest', 'linear'
+        value = (check_enum(value[0], 'tex interpolation', valid),
+                 check_enum(value[1], 'tex interpolation', valid))
+        self._interpolation = value
+        self._glir.command('INTERPOLATION', self._id, *value)
 
-    def resize(self, shape):
-        """ Resize the texture (deferred operation)
+    def resize(self, shape, format=None, internalformat=None):
+        """Set the texture size and format
 
         Parameters
         ----------
-
         shape : tuple of integers
-            New texture shape
+            New texture shape in zyx order. Optionally, an extra dimention
+            may be specified to indicate the number of color channels.
+        format : str | enum | None
+            The format of the texture: 'luminance', 'alpha',
+            'luminance_alpha', 'rgb', or 'rgba'. If not given the format
+            is chosen automatically based on the number of channels.
+            When the data has one channel, 'luminance' is assumed.
+        internalformat : str | enum | None
+            The internal (storage) format of the texture: 'luminance',
+            'alpha', 'r8', 'r16', 'r16f', 'r32f'; 'luminance_alpha',
+            'rg8', 'rg16', 'rg16f', 'rg32f'; 'rgb', 'rgb8', 'rgb16',
+            'rgb16f', 'rgb32f'; 'rgba', 'rgba8', 'rgba16', 'rgba16f',
+            'rgba32f'.  If None, the internalformat is chosen
+            automatically based on the number of channels.  This is a
+            hint which may be ignored by the OpenGL implementation.
+        """
+        return self._resize(shape, format, internalformat)
 
-        Notes
-        -----
-        This clears any pending operations.
+    def _resize(self, shape, format=None, internalformat=None):
+        """Internal method for resize.
         """
         shape = self._normalize_shape(shape)
 
-        if not self._resizeable:
-            raise RuntimeError("Texture is not resizeable")
+        # Check
+        if not self._resizable:
+            raise RuntimeError("Texture is not resizable")
 
-        if self._base is not None:
-            raise RuntimeError("Texture view is not resizeable")
-
-        if len(shape) != len(self.shape):
-            raise ValueError("New shape has wrong number of dimensions")
-
-        if shape == self.shape:
-            return
+        # Determine format
+        if format is None:
+            format = self._formats[shape[-1]]
+            # Keep current format if channels match
+            if self._format and \
+               self._inv_formats[self._format] == self._inv_formats[format]:
+                format = self._format
+        else:
+            format = check_enum(format)
 
-        # Reset format if size of last dimension differs
-        if shape[-1] != self.shape[-1]:
-            format = BaseTexture._formats.get(shape[-1], None)
-            if format is None:
-                raise ValueError("Cannot determine texture format from shape")
-            self._format = format
+        if internalformat is None:
+            # Keep current internalformat if channels match
+            if self._internalformat and \
+               self._inv_internalformats[self._internalformat] == shape[-1]:
+                internalformat = self._internalformat
+        else:
 
-        # Invalidate any view on this texture
-        for view in self._views:
-            view._valid = False
-        self._views = []
+            internalformat = check_enum(internalformat)
 
-        self._pending_data = []
-        self._need_resize = True
+        # Check
+        if format not in self._inv_formats:
+            raise ValueError('Invalid texture format: %r.' % format)
+        elif shape[-1] != self._inv_formats[format]:
+            raise ValueError('Format does not match with given shape.')
+        
+        if internalformat is None:
+            pass
+        elif internalformat not in self._inv_internalformats:
+            raise ValueError(
+                'Invalid texture internalformat: %r.' 
+                % internalformat
+            )
+        elif shape[-1] != self._inv_internalformats[internalformat]:
+            raise ValueError('Internalformat does not match with given shape.')
+
+        # Store and send GLIR command
         self._shape = shape
-        if self._data is not None and self._store:
-            self._data = np.resize(self._data, self._shape)
-        else:
-            self._data = None
+        self._format = format
+        self._internalformat = internalformat
+        self._glir.command('SIZE', self._id, self._shape, self._format, 
+                           self._internalformat)
 
     def set_data(self, data, offset=None, copy=False):
-        """
-        Set data (deferred operation)
+        """Set texture data
 
         Parameters
         ----------
-
         data : ndarray
             Data to be uploaded
-        offset: int or tuple of ints
+        offset: int | tuple of ints
             Offset in texture where to start copying data
         copy: bool
             Since the operation is deferred, data may change before
-            data is actually uploaded to GPU memory.
-            Asking explicitly for a copy will prevent this behavior.
+            data is actually uploaded to GPU memory. Asking explicitly
+            for a copy will prevent this behavior.
 
         Notes
         -----
-        This operation implicitely resizes the texture to the shape of the data
-        if given offset is None.
+        This operation implicitely resizes the texture to the shape of
+        the data if given offset is None.
         """
-        if self.base is not None and not self._valid:
-            raise ValueError("This texture view has been invalidated")
-
-        if self.base is not None:
-            self.base.set_data(data, offset=self.offset, copy=copy)
-            return
-
-        # Force using the same data type. We could probably allow it,
-        # but with the views and data storage, this is rather complex.
-        if data.dtype != self.dtype:
-            raise ValueError('Cannot set texture data with another dtype.')
+        return self._set_data(data, offset, copy)
 
+    def _set_data(self, data, offset=None, copy=False):
+        """Internal method for set_data.
+        """
+        
         # Copy if needed, check/normalize shape
         data = np.array(data, copy=copy)
         data = self._normalize_shape(data)
-
-        # Check data has the right shape
-        # if len(data.shape) != len(self.shape):
-        #  raise ValueError("Data has wrong shape")
-
-        # Check if resize needed
+        
+        # Maybe resize to purge DATA commands?
         if offset is None:
-            if data.shape != self.shape:
-                self.resize(data.shape)
-
-        if offset is None or offset == (0,) * len(self.shape):
-            if data.shape == self.shape:
-                self._pending_data = []
-
-            # Convert offset to something usable
-            offset = (0,) * len(self.shape)
-
+            self._resize(data.shape)
+        elif all([i == 0 for i in offset]) and data.shape == self._shape:
+            self._resize(data.shape)
+        
+        # Convert offset to something usable
+        offset = offset or tuple([0 for i in range(self._ndim)])
+        assert len(offset) == self._ndim
+        
         # Check if data fits
-        for i in range(len(data.shape)):
-            if offset[i] + data.shape[i] > self.shape[i]:
+        for i in range(len(data.shape)-1):
+            if offset[i] + data.shape[i] > self._shape[i]:
                 raise ValueError("Data is too large")
-
-        if self._store:
-            pass
-            # todo: @nico should we not update self._data?
-            # but we need to keep the offset into account.
-
-        self._pending_data.append((data, offset))
-
-    def __getitem__(self, key):
+        
+        # Send GLIR command
+        self._glir.command('DATA', self._id, offset, data)
+    
+    def __setitem__(self, key, data):
         """ x.__getitem__(y) <==> x[y] """
-        if self.base is not None:
-            raise ValueError("Can only access data from a base texture")
-
+        
         # Make sure key is a tuple
         if isinstance(key, (int, slice)) or key == Ellipsis:
             key = (key,)
 
         # Default is to access the whole texture
-        shape = self.shape
+        shape = self._shape
         slices = [slice(0, shape[i]) for i in range(len(shape))]
 
         # Check last key/Ellipsis to decide on the order
@@ -424,12 +337,12 @@ class BaseTexture(GLObject):
         dims = range(0, len(key))
         if key[0] == Ellipsis:
             keys = key[::-1]
-            dims = range(len(self.shape) - 1,
-                         len(self.shape) - 1 - len(keys), -1)
+            dims = range(len(self._shape) - 1,
+                         len(self._shape) - 1 - len(keys), -1)
 
         # Find exact range for each key
         for k, dim in zip(keys, dims):
-            size = self.shape[dim]
+            size = self._shape[dim]
             if isinstance(k, int):
                 if k < 0:
                     k += size
@@ -440,7 +353,7 @@ class BaseTexture(GLObject):
             elif isinstance(k, slice):
                 start, stop, step = k.indices(size)
                 if step != 1:
-                    raise ValueError("Cannot access non-contiguous data")
+                    raise IndexError("Cannot access non-contiguous data")
                 if stop < start:
                     start, stop = stop, start
                 slices[dim] = slice(start, stop, step)
@@ -449,160 +362,87 @@ class BaseTexture(GLObject):
             else:
                 raise TypeError("Texture indices must be integers")
 
-        offset = tuple([s.start for s in slices])
+        offset = tuple([s.start for s in slices])[:self._ndim]
         shape = tuple([s.stop - s.start for s in slices])
-        data = None
-        if self.data is not None:
-            data = self.data[slices]
-
-        T = self.__class__(dtype=self.dtype, shape=shape,
-                           base=self, offset=offset, resizeable=False)
-        T._data = data
-        self._views.append(T)
-        return T
+        size = np.prod(shape) if len(shape) > 0 else 1
+        
+        # Make sure data is an array
+        if not isinstance(data, np.ndarray):
+            data = np.array(data, copy=False)
+        # Make sure data is big enough
+        if data.shape != shape:
+            data = np.resize(data, shape)
 
-    def __setitem__(self, key, data):
-        """ x.__getitem__(y) <==> x[y] """
-        if self.base is not None and not self._valid:
-            raise ValueError("This texture view has been invalited")
+        # Set data (deferred)
+        self._set_data(data=data, offset=offset, copy=False)
+    
+    def __repr__(self):
+        return "<%s shape=%r format=%r at 0x%x>" % (
+            self.__class__.__name__, self._shape, self._format, id(self))
 
-        # Make sure key is a tuple
-        if isinstance(key, (int, slice)) or key == Ellipsis:
-            key = (key,)
 
-        # Default is to access the whole texture
-        shape = self.shape
-        slices = [slice(0, shape[i]) for i in range(len(shape))]
+# --------------------------------------------------------- Texture1D class ---
+class Texture1D(BaseTexture):
+    """ One dimensional texture
 
-        # Check last key/Ellipsis to decide on the order
-        keys = key[::+1]
-        dims = range(0, len(key))
-        if key[0] == Ellipsis:
-            keys = key[::-1]
-            dims = range(len(self.shape) - 1,
-                         len(self.shape) - 1 - len(keys), -1)
+    Parameters
+    ----------
+    data : ndarray | tuple | None
+        Texture data in the form of a numpy array (or something that
+        can be turned into one). A tuple with the shape of the texture
+        can also be given.
+    format : str | enum | None
+        The format of the texture: 'luminance', 'alpha',
+        'luminance_alpha', 'rgb', or 'rgba'. If not given the format
+        is chosen automatically based on the number of channels.
+        When the data has one channel, 'luminance' is assumed.
+    resizable : bool
+        Indicates whether texture can be resized. Default True.
+    interpolation : str | None
+        Interpolation mode, must be one of: 'nearest', 'linear'.
+        Default 'nearest'.
+    wrapping : str | None
+        Wrapping mode, must be one of: 'repeat', 'clamp_to_edge',
+        'mirrored_repeat'. Default 'clamp_to_edge'.
+    shape : tuple | None
+        Optional. A tuple with the shape of the texture. If ``data``
+        is also a tuple, it will override the value of ``shape``.
+    internalformat : str | None
+        Internal format to use.
+    resizeable : None
+        Deprecated version of `resizable`.
+    """
+    _ndim = 1
+    _GLIR_TYPE = 'Texture1D'
 
-        # Find exact range for each key
-        for k, dim in zip(keys, dims):
-            size = self.shape[dim]
-            if isinstance(k, int):
-                if k < 0:
-                    k += size
-                if k < 0 or k > size:
-                    raise IndexError("Texture assignment index out of range")
-                start, stop = k, k + 1
-                slices[dim] = slice(start, stop, 1)
-            elif isinstance(k, slice):
-                start, stop, step = k.indices(size)
-                if step != 1:
-                    raise ValueError("Cannot access non-contiguous data")
-                if stop < start:
-                    start, stop = stop, start
-                slices[dim] = slice(start, stop, step)
-            elif k == Ellipsis:
-                pass
-            else:
-                raise TypeError("Texture indices must be integers")
+    def __init__(self, data=None, format=None, resizable=True,
+                 interpolation=None, wrapping=None, shape=None,
+                 internalformat=None, resizeable=None):
+        BaseTexture.__init__(self, data, format, resizable, interpolation,
+                             wrapping, shape, internalformat, resizeable)
 
-        offset = tuple([s.start for s in slices])
-        shape = tuple([s.stop - s.start for s in slices])
-        size = np.prod(shape) if len(shape) > 0 else 1
+    @property
+    def width(self):
+        """ Texture width """
+        return self._shape[0]
 
-        # We have CPU storage
-        if self.data is not None:
-            self.data[key] = data
-            data = self.data[key]
-        else:
-            # Make sure data is an array
-            if not isinstance(data, np.ndarray):
-                data = np.array(data, dtype=self.dtype, copy=False)
-            # Make sure data is big enough
-            if data.size != size:
-                data = np.resize(data, size).reshape(shape)
+    @property
+    def glsl_type(self):
+        """ GLSL declaration strings required for a variable to hold this data.
+        """
+        return 'uniform', 'sampler1D'
 
-        # Set data (deferred)
-        if self.base is None:
-            self.set_data(data=data, offset=offset, copy=False)
-        else:
-            offset = self.offset + offset
-            self.base.set_data(data=data, offset=offset, copy=False)
-
-    def _parameterize(self):
-        """ Paramaterize texture """
-        gl.glTexParameterf(self._target, gl.GL_TEXTURE_MIN_FILTER,
-                           self._interpolation[0])
-        gl.glTexParameterf(self._target, gl.GL_TEXTURE_MAG_FILTER,
-                           self._interpolation[1])
-        gl.glTexParameterf(self._target, gl.GL_TEXTURE_WRAP_S,
-                           self._wrapping[0])
-        gl.glTexParameterf(self._target, gl.GL_TEXTURE_WRAP_T,
-                           self._wrapping[1])
-
-    def _create(self):
-        """ Create texture on GPU """
-        logger.debug("GPU: Creating texture")
-        self._handle = gl.glCreateTexture()
-
-    def _delete(self):
-        """ Delete texture from GPU """
-        logger.debug("GPU: Deleting texture")
-        gl.glDeleteTexture(self._handle)
-
-    def _activate(self):
-        """ Activate texture on GPU """
-        logger.debug("GPU: Activate texture")
-        gl.glBindTexture(self.target, self._handle)
-
-        # We let base texture to handle all operations
-        if self.base is not None:
-            return
-
-        # Resize if necessary
-        if self._need_resize:
-            self._resize()
-            self._need_resize = False
-
-        # Reparameterize if necessary
-        if self._need_parameterization:
-            self._parameterize()
-            self._need_parameterization = False
-
-        # Update pending data if necessary
-        if self._pending_data:
-            logger.debug("GPU: Updating texture (%d pending operation(s))" %
-                         len(self._pending_data))
-            self._update_data()
-
-    def _deactivate(self):
-        """ Deactivate texture on GPU """
-        logger.debug("GPU: Deactivate texture")
-        gl.glBindTexture(self._target, 0)
-
-    # Taken from pygly
-    def _get_alignment(self, width):
-        """Determines a textures byte alignment.
-
-        If the width isn't a power of 2
-        we need to adjust the byte alignment of the image.
-        The image height is unimportant
-
-        www.opengl.org/wiki/Common_Mistakes#Texture_upload_and_pixel_reads
+    @property
+    def glsl_sampler_type(self):
+        """ GLSL type of the sampler.
         """
-        # we know the alignment is appropriate
-        # if we can divide the width by the
-        # alignment cleanly
-        # valid alignments are 1,2,4 and 8
-        # put 4 first, since it's the default
-        alignments = [4, 8, 2, 1]
-        for alignment in alignments:
-            if width % alignment == 0:
-                return alignment
+        return 'sampler1D'
 
-    def __repr__(self):
-        return "<%s shape=%r dtype=%r format=%r target=%r at 0x%x>" % (
-            self.__class__.__name__,
-            self._shape, self._dtype, self._format, self._target,
-            id(self))
+    @property
+    def glsl_sample(self):
+        """ GLSL function that samples the texture.
+        """
+        return 'texture1D'
 
 
 # --------------------------------------------------------- Texture2D class ---
@@ -611,39 +451,38 @@ class Texture2D(BaseTexture):
 
     Parameters
     ----------
-
     data : ndarray
-        Texture data (optional), shaped as HxW.
-    shape : tuple of integers
-        Texture shape (optional), with shape HxW.
-    dtype : dtype
-        Texture data type (optional)
-    store : bool
-        Specify whether this object stores a reference to the data,
-        allowing the data to be updated regardless of striding. Note
-        that modifying the data after passing it here might result in
-        undesired behavior, unless a copy is given. Default True.
-    format : str | ENUM
-        The format of the texture: 'luminance', 'alpha', 'luminance_alpha',
-        'rgb', or 'rgba' (or ENUMs GL_LUMINANCE, ALPHA, GL_LUMINANCE_ALPHA,
-        or GL_RGB, GL_RGBA). If not given the format is chosen automatically
-        based on the number of channels. When the data has one channel,
-        'luminance' is assumed.
+        Texture data shaped as W, or a tuple with the shape for
+        the texture (W).
+    format : str | enum | None
+        The format of the texture: 'luminance', 'alpha',
+        'luminance_alpha', 'rgb', or 'rgba'. If not given the format
+        is chosen automatically based on the number of channels.
+        When the data has one channel, 'luminance' is assumed.
+    resizable : bool
+        Indicates whether texture can be resized. Default True.
+    interpolation : str
+        Interpolation mode, must be one of: 'nearest', 'linear'.
+        Default 'nearest'.
+    wrapping : str
+        Wrapping mode, must be one of: 'repeat', 'clamp_to_edge',
+        'mirrored_repeat'. Default 'clamp_to_edge'.
+    shape : tuple
+        Optional. A tuple with the shape HxW. If ``data``
+        is also a tuple, it will override the value of ``shape``.
+    internalformat : str | None
+        Internal format to use.
+    resizeable : None
+        Deprecated version of `resizable`.
     """
     _ndim = 2
+    _GLIR_TYPE = 'Texture2D'
 
-    def __init__(self, data=None, shape=None, dtype=None, store=True,
-                 format=None, **kwargs):
-
-        # We don't want these parameters to be seen from outside (because they
-        # are only used internally)
-        offset = kwargs.get("offset", None)
-        base = kwargs.get("base", None)
-        resizeable = kwargs.get("resizeable", True)
-        BaseTexture.__init__(self, data=data, shape=shape, dtype=dtype,
-                             base=base, resizeable=resizeable, store=store,
-                             target=gl.GL_TEXTURE_2D, offset=offset,
-                             format=format)
+    def __init__(self, data=None, format=None, resizable=True,
+                 interpolation=None, wrapping=None, shape=None,
+                 internalformat=None, resizeable=None):
+        BaseTexture.__init__(self, data, format, resizable, interpolation,
+                             wrapping, shape, internalformat, resizeable)
 
     @property
     def height(self):
@@ -661,30 +500,17 @@ class Texture2D(BaseTexture):
         """
         return 'uniform', 'sampler2D'
 
-    def _resize(self):
-        """ Texture resize on GPU """
-        logger.debug("GPU: Resizing texture(%sx%s)" %
-                     (self.width, self.height))
-        shape = self.height, self.width
-        gl.glTexImage2D(self.target, 0, self._format, self._format,
-                        self._gtype, shape)
-
-    def _update_data(self):
-        """ Texture update on GPU """
-        # Update data
-        while self._pending_data:
-            data, offset = self._pending_data.pop(0)
-            x = y = 0
-            if offset is not None:
-                y, x = offset[0], offset[1]
-            # Set alignment (width is nbytes_per_pixel * npixels_per_line)
-            alignment = self._get_alignment(data.shape[-2]*data.shape[-1])
-            if alignment != 4:
-                gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, alignment)
-            gl.glTexSubImage2D(self.target, 0, x, y, self._format,
-                               self._gtype, data)
-            if alignment != 4:
-                gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 4)
+    @property
+    def glsl_sampler_type(self):
+        """ GLSL type of the sampler.
+        """
+        return 'sampler2D'
+
+    @property
+    def glsl_sample(self):
+        """ GLSL function that samples the texture.
+        """
+        return 'texture2D'
 
 
 # --------------------------------------------------------- Texture3D class ---
@@ -693,41 +519,39 @@ class Texture3D(BaseTexture):
 
     Parameters
     ----------
-    data : ndarray
-        Texture data (optional), shaped as DxHxW.
-    shape : tuple of integers
-        Texture shape (optional) DxHxW.
-    dtype : dtype
-        Texture data type (optional)
-    store : bool
-        Specify whether this object stores a reference to the data,
-        allowing the data to be updated regardless of striding. Note
-        that modifying the data after passing it here might result in
-        undesired behavior, unless a copy is given. Default True.
-    format : str | ENUM
-        The format of the texture: 'luminance', 'alpha', 'luminance_alpha',
-        'rgb', or 'rgba' (or ENUMs GL_LUMINANCE, ALPHA, GL_LUMINANCE_ALPHA,
-        or GL_RGB, GL_RGBA). If not given the format is chosen automatically
-        based on the number of channels. When the data has one channel,
-        'luminance' is assumed.
+    data : ndarray | tuple | None
+        Texture data in the form of a numpy array (or something that
+        can be turned into one). A tuple with the shape of the texture
+        can also be given.
+    format : str | enum | None
+        The format of the texture: 'luminance', 'alpha',
+        'luminance_alpha', 'rgb', or 'rgba'. If not given the format
+        is chosen automatically based on the number of channels.
+        When the data has one channel, 'luminance' is assumed.
+    resizable : bool
+        Indicates whether texture can be resized. Default True.
+    interpolation : str | None
+        Interpolation mode, must be one of: 'nearest', 'linear'.
+        Default 'nearest'.
+    wrapping : str | None
+        Wrapping mode, must be one of: 'repeat', 'clamp_to_edge',
+        'mirrored_repeat'. Default 'clamp_to_edge'.
+    shape : tuple | None
+        Optional. A tuple with the shape of the texture. If ``data``
+        is also a tuple, it will override the value of ``shape``.
+    internalformat : str | None
+        Internal format to use.
+    resizeable : None
+        Deprecated version of `resizable`.
     """
     _ndim = 3
+    _GLIR_TYPE = 'Texture3D'
 
-    def __init__(self, data=None, shape=None, dtype=None, store=True,
-                 format=None, **kwargs):
-
-        # Import from PyOpenGL
-        _gl = _check_pyopengl_3D()
-
-        # We don't want these parameters to be seen from outside (because they
-        # are only used internally)
-        offset = kwargs.get("offset", None)
-        base = kwargs.get("base", None)
-        resizeable = kwargs.get("resizeable", True)
-        BaseTexture.__init__(self, data=data, shape=shape, dtype=dtype,
-                             base=base, resizeable=resizeable, store=store,
-                             target=_gl.GL_TEXTURE_3D, offset=offset,
-                             format=format)
+    def __init__(self, data=None, format=None, resizable=True,
+                 interpolation=None, wrapping=None, shape=None,
+                 internalformat=None, resizeable=None):
+        BaseTexture.__init__(self, data, format, resizable, interpolation,
+                             wrapping, shape, internalformat, resizeable)
 
     @property
     def width(self):
@@ -750,29 +574,226 @@ class Texture3D(BaseTexture):
         """
         return 'uniform', 'sampler3D'
 
-    def _resize(self):
-        """ Texture resize on GPU """
-        logger.debug("GPU: Resizing texture(%sx%sx%s)" %
-                     (self.depth, self.height, self.width))
-        glTexImage3D(self.target, 0, self._format, self._format,
-                     self._gtype, (self.depth, self.height, self.width))
-
-    def _update_data(self):
-        """ Texture update on GPU """
-        while self._pending_data:
-            data, offset = self._pending_data.pop(0)
-            z = y = x = 0
-            if offset is not None:
-                z, y, x = offset[0], offset[1], offset[2]
-            # Set alignment (width is nbytes_per_pixel * npixels_per_line)
-            alignment = self._get_alignment(data.shape[-3] *
-                                            data.shape[-2] * data.shape[-1])
-            if alignment != 4:
-                gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, alignment)
-            glTexSubImage3D(self.target, 0, x, y, z, self._format,
-                            self._gtype, data)
-            if alignment != 4:
-                gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 4)
+    @property
+    def glsl_sampler_type(self):
+        """ GLSL type of the sampler.
+        """
+        return 'sampler3D'
+
+    @property
+    def glsl_sample(self):
+        """ GLSL function that samples the texture.
+        """
+        return 'texture3D'
+
+
+# ------------------------------------------------- TextureEmulated3D class ---
+class TextureEmulated3D(Texture2D):
+    """ Two dimensional texture that is emulating a three dimensional texture
+
+    Parameters
+    ----------
+    data : ndarray | tuple | None
+        Texture data in the form of a numpy array (or something that
+        can be turned into one). A tuple with the shape of the texture
+        can also be given.
+    format : str | enum | None
+        The format of the texture: 'luminance', 'alpha',
+        'luminance_alpha', 'rgb', or 'rgba'. If not given the format
+        is chosen automatically based on the number of channels.
+        When the data has one channel, 'luminance' is assumed.
+    resizable : bool
+        Indicates whether texture can be resized. Default True.
+    interpolation : str | None
+        Interpolation mode, must be one of: 'nearest', 'linear'.
+        Default 'nearest'.
+    wrapping : str | None
+        Wrapping mode, must be one of: 'repeat', 'clamp_to_edge',
+        'mirrored_repeat'. Default 'clamp_to_edge'.
+    shape : tuple | None
+        Optional. A tuple with the shape of the texture. If ``data``
+        is also a tuple, it will override the value of ``shape``.
+    internalformat : str | None
+        Internal format to use.
+    resizeable : None
+        Deprecated version of `resizable`.
+    """
+
+    # TODO: does GL's nearest use floor or round?
+    _glsl_sample_nearest = """
+        vec4 sample(sampler2D tex, vec3 texcoord) {
+            // Don't let adjacent frames be interpolated into this one
+            texcoord.x = min(texcoord.x * $shape.x, $shape.x - 0.5);
+            texcoord.x = max(0.5, texcoord.x) / $shape.x;
+            texcoord.y = min(texcoord.y * $shape.y, $shape.y - 0.5);
+            texcoord.y = max(0.5, texcoord.y) / $shape.y;
+
+            float index = floor(texcoord.z * $shape.z);
+
+            // Do a lookup in the 2D texture
+            float u = (mod(index, $r) + texcoord.x) / $r;
+            float v = (floor(index / $r) + texcoord.y) / $c;
+
+            return texture2D(tex, vec2(u,v));
+        }
+    """
+
+    _glsl_sample_linear = """
+        vec4 sample(sampler2D tex, vec3 texcoord) {
+            // Don't let adjacent frames be interpolated into this one
+            texcoord.x = min(texcoord.x * $shape.x, $shape.x - 0.5);
+            texcoord.x = max(0.5, texcoord.x) / $shape.x;
+            texcoord.y = min(texcoord.y * $shape.y, $shape.y - 0.5);
+            texcoord.y = max(0.5, texcoord.y) / $shape.y;
+
+            float z = texcoord.z * $shape.z;
+            float zindex1 = floor(z);
+            float u1 = (mod(zindex1, $r) + texcoord.x) / $r;
+            float v1 = (floor(zindex1 / $r) + texcoord.y) / $c;
+
+            float zindex2 = zindex1 + 1.0;
+            float u2 = (mod(zindex2, $r) + texcoord.x) / $r;
+            float v2 = (floor(zindex2 / $r) + texcoord.y) / $c;
+
+            vec4 s1 = texture2D(tex, vec2(u1, v1));
+            vec4 s2 = texture2D(tex, vec2(u2, v2));
+
+            return s1 * (zindex2 - z) + s2 * (z - zindex1);
+        }
+    """
+
+    _gl_max_texture_size = 1024  # For now, we just set this manually
+
+    def __init__(self, data=None, format=None, resizable=True,
+                 interpolation=None, wrapping=None, shape=None,
+                 internalformat=None, resizeable=None):
+        from ..visuals.shaders import Function
+
+        self._set_emulated_shape(data)
+        Texture2D.__init__(self, self._normalize_emulated_shape(data),
+                           format, resizable, interpolation, wrapping,
+                           shape, internalformat, resizeable)
+        if self.interpolation == 'nearest':
+            self._glsl_sample = Function(self.__class__._glsl_sample_nearest)
+        else:
+            self._glsl_sample = Function(self.__class__._glsl_sample_linear)
+        self._update_variables()
+
+    def _set_emulated_shape(self, data_or_shape):
+        if isinstance(data_or_shape, np.ndarray):
+            self._emulated_shape = data_or_shape.shape
+        else:
+            assert isinstance(data_or_shape, tuple)
+            self._emulated_shape = tuple(data_or_shape)
+
+        depth, width = self._emulated_shape[0], self._emulated_shape[1]
+        self._r = TextureEmulated3D._gl_max_texture_size // width
+        self._c = depth // self._r
+        if math.fmod(depth, self._r):
+            self._c += 1
+
+    def _normalize_emulated_shape(self, data_or_shape):
+        if isinstance(data_or_shape, np.ndarray):
+            new_shape = self._normalize_emulated_shape(data_or_shape.shape)
+            new_data = np.empty(new_shape, dtype=data_or_shape.dtype)
+            for j in range(self._c):
+                for i in range(self._r):
+                    i0, i1 = i * self.width, (i+1) * self.width
+                    j0, j1 = j * self.height, (j+1) * self.height
+                    k = j * self._r + i
+                    if k >= self.depth:
+                        break
+                    new_data[j0:j1, i0:i1] = data_or_shape[k]
+
+            return new_data
+
+        assert isinstance(data_or_shape, tuple)
+        return (self._c * self.height, self._r * self.width) + \
+            data_or_shape[3:]
+
+    def _update_variables(self):
+        self._glsl_sample['shape'] = self.shape[:3][::-1]
+        self._glsl_sample['c'] = self._c
+        self._glsl_sample['r'] = self._r
+
+    def set_data(self, data, offset=None, copy=False):
+        """Set texture data
+
+        Parameters
+        ----------
+        data : ndarray
+            Data to be uploaded
+        offset: int | tuple of ints
+            Offset in texture where to start copying data
+        copy: bool
+            Since the operation is deferred, data may change before
+            data is actually uploaded to GPU memory. Asking explicitly
+            for a copy will prevent this behavior.
+
+        Notes
+        -----
+        This operation implicitely resizes the texture to the shape of
+        the data if given offset is None.
+        """
+        self._set_emulated_shape(data)
+        Texture2D.set_data(self, self._normalize_emulated_shape(data),
+                           offset, copy)
+        self._update_variables()
+
+    def resize(self, shape, format=None, internalformat=None):
+        """Set the texture size and format
+
+        Parameters
+        ----------
+        shape : tuple of integers
+            New texture shape in zyx order. Optionally, an extra dimention
+            may be specified to indicate the number of color channels.
+        format : str | enum | None
+            The format of the texture: 'luminance', 'alpha',
+            'luminance_alpha', 'rgb', or 'rgba'. If not given the format
+            is chosen automatically based on the number of channels.
+            When the data has one channel, 'luminance' is assumed.
+        internalformat : str | enum | None
+            The internal (storage) format of the texture: 'luminance',
+            'alpha', 'r8', 'r16', 'r16f', 'r32f'; 'luminance_alpha',
+            'rg8', 'rg16', 'rg16f', 'rg32f'; 'rgb', 'rgb8', 'rgb16',
+            'rgb16f', 'rgb32f'; 'rgba', 'rgba8', 'rgba16', 'rgba16f',
+            'rgba32f'.  If None, the internalformat is chosen
+            automatically based on the number of channels.  This is a
+            hint which may be ignored by the OpenGL implementation.
+        """
+        self._set_emulated_shape(shape)
+        Texture2D.resize(self, self._normalize_emulated_shape(shape),
+                         format, internalformat)
+        self._update_variables()
+
+    @property
+    def shape(self):
+        """ Data shape (last dimension indicates number of color channels)
+        """
+        return self._emulated_shape
+
+    @property
+    def width(self):
+        """ Texture width """
+        return self._emulated_shape[2]
+
+    @property
+    def height(self):
+        """ Texture height """
+        return self._emulated_shape[1]
+
+    @property
+    def depth(self):
+        """ Texture depth """
+        return self._emulated_shape[0]
+
+    @property
+    def glsl_sample(self):
+        """ GLSL function that samples the texture.
+        """
+
+        return self._glsl_sample
 
 
 # ------------------------------------------------------ TextureAtlas class ---
@@ -788,7 +809,7 @@ class TextureAtlas(Texture2D):
     Parameters
     ----------
     shape : tuple of int
-        Texture width and height (optional).
+        Texture shape (optional).
 
     Notes
     -----
@@ -805,9 +826,8 @@ class TextureAtlas(Texture2D):
         shape = tuple(2 ** (np.log2(shape) + 0.5).astype(int)) + (3,)
         self._atlas_nodes = [(0, 0, shape[1])]
         data = np.zeros(shape, np.float32)
-        super(TextureAtlas, self).__init__(data)
-        self.interpolation = 'linear'
-        self.wrapping = 'clamp_to_edge'
+        super(TextureAtlas, self).__init__(data, interpolation='linear',
+                                           wrapping='clamp_to_edge')
 
     def get_free_region(self, width, height):
         """Get a free region of given size and allocate it
@@ -877,13 +897,13 @@ class TextureAtlas(Texture2D):
         node = self._atlas_nodes[index]
         x, y = node[0], node[1]
         width_left = width
-        if x+width > self.shape[1]:
+        if x+width > self._shape[1]:
             return -1
         i = index
         while width_left > 0:
             node = self._atlas_nodes[i]
             y = max(y, node[1])
-            if y+height > self.shape[0]:
+            if y+height > self._shape[0]:
                 return -1
             width_left -= node[2]
             i += 1
diff --git a/vispy/gloo/util.py b/vispy/gloo/util.py
index fa745fb..46052ef 100644
--- a/vispy/gloo/util.py
+++ b/vispy/gloo/util.py
@@ -1,9 +1,10 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
+from ..ext.six import string_types
 from .wrappers import read_pixels
 
 
@@ -64,7 +65,7 @@ def check_variable(name):
 def check_identifier(name):
     if '__' in name:
         return "Identifiers may not contain double-underscores."
-
+    
     if name[:3] == 'gl_' or name[:3] == 'GL_':
         return "Identifiers may not begin with gl_ or GL_."
 
@@ -72,6 +73,27 @@ def check_identifier(name):
         return "Identifier is a reserved keyword."
 
 
+def check_enum(enum, name=None, valid=None):
+    """ Get lowercase string representation of enum.
+    """
+    name = name or 'enum'
+    # Try to convert
+    res = None
+    if isinstance(enum, int):
+        if hasattr(enum, 'name') and enum.name.startswith('GL_'):
+            res = enum.name[3:].lower()
+    elif isinstance(enum, string_types):
+        res = enum.lower()
+    # Check
+    if res is None:
+        raise ValueError('Could not determine string represenatation for'
+                         'enum %r' % enum)
+    elif valid and res not in valid:
+        raise ValueError('Value of %s must be one of %r, not %r' % 
+                         (name, valid, enum))
+    return res
+
+
 vert_draw = """
 attribute vec2 a_position;
 attribute vec2 a_texcoord;
diff --git a/vispy/gloo/variable.py b/vispy/gloo/variable.py
deleted file mode 100644
index 7431f15..0000000
--- a/vispy/gloo/variable.py
+++ /dev/null
@@ -1,386 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-# -----------------------------------------------------------------------------
-import numpy as np
-
-from . import gl
-from .globject import GLObject
-from .buffer import VertexBuffer, DataBufferView
-from .texture import BaseTexture, Texture2D, Texture3D, GL_SAMPLER_3D
-from .framebuffer import RenderBuffer
-from ..util import logger
-from .util import check_variable
-
-
-# ------------------------------------------------------------- gl_typeinfo ---
-gl_typeinfo = {
-    gl.GL_FLOAT: (1, gl.GL_FLOAT,        np.float32),
-    gl.GL_FLOAT_VEC2: (2, gl.GL_FLOAT,        np.float32),
-    gl.GL_FLOAT_VEC3: (3, gl.GL_FLOAT,        np.float32),
-    gl.GL_FLOAT_VEC4: (4, gl.GL_FLOAT,        np.float32),
-    gl.GL_INT: (1, gl.GL_INT,          np.int32),
-    gl.GL_INT_VEC2: (2, gl.GL_INT,          np.int32),
-    gl.GL_INT_VEC3: (3, gl.GL_INT,          np.int32),
-    gl.GL_INT_VEC4: (4, gl.GL_INT,          np.int32),
-    gl.GL_BOOL: (1, gl.GL_BOOL,         np.bool),
-    gl.GL_BOOL_VEC2: (2, gl.GL_BOOL,         np.bool),
-    gl.GL_BOOL_VEC3: (3, gl.GL_BOOL,         np.bool),
-    gl.GL_BOOL_VEC4: (4, gl.GL_BOOL,         np.bool),
-    gl.GL_FLOAT_MAT2: (4, gl.GL_FLOAT,        np.float32),
-    gl.GL_FLOAT_MAT3: (9, gl.GL_FLOAT,        np.float32),
-    gl.GL_FLOAT_MAT4: (16, gl.GL_FLOAT,        np.float32),
-    #    gl.GL_SAMPLER_1D   : ( 1, gl.GL_UNSIGNED_INT, np.uint32),
-    gl.GL_SAMPLER_2D: (1, gl.GL_UNSIGNED_INT, np.uint32),
-    GL_SAMPLER_3D: (1, gl.GL_UNSIGNED_INT, np.uint32)
-}
-
-
-# ---------------------------------------------------------- Variable class ---
-class Variable(GLObject):
-    """ A variable is an interface between a program and some data 
-    
-    For internal use
-    
-    Parameters
-    ----------
-    
-    program : Program
-        The Program instance to which the data applies
-    name : str
-        The variable name
-    gtype : ENUM
-        The type of the variable: GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3,
-        GL_FLOAT_VEC4, GL_INT, GL_BOOL, GL_FLOAT_MAT2, GL_FLOAT_MAT3,
-        GL_FLOAT_MAT4, gl.GL_SAMPLER_2D, or GL_SAMPLER_3D
-    
-    """
-
-    def __init__(self, program, name, gtype):
-        """ Initialize the data into default state """
-
-        # Make sure variable type is allowed (for ES 2.0 shader)
-        if gtype not in [gl.GL_FLOAT,
-                         gl.GL_FLOAT_VEC2,
-                         gl.GL_FLOAT_VEC3,
-                         gl.GL_FLOAT_VEC4,
-                         gl.GL_INT,
-                         gl.GL_BOOL,
-                         gl.GL_FLOAT_MAT2,
-                         gl.GL_FLOAT_MAT3,
-                         gl.GL_FLOAT_MAT4,
-                         #                         gl.GL_SAMPLER_1D,
-                         gl.GL_SAMPLER_2D,
-                         GL_SAMPLER_3D]:
-            raise TypeError("Unknown variable type")
-
-        GLObject.__init__(self)
-
-        # Program this variable belongs to
-        self._program = program
-
-        # Name of this variable in the program
-        self._name = name
-        check = check_variable(name)
-        if check:
-            logger.warning('Invalid variable name "%s". (%s)'
-                           % (name, check))
-
-        # Build dtype
-        size, _, base = gl_typeinfo[gtype]
-        self._dtype = (name, base, size)
-
-        # GL type
-        self._gtype = gtype
-
-        # CPU data
-        self._data = None
-
-        # Whether this variable is actually being used by GLSL
-        self._enabled = True
-
-    @property
-    def name(self):
-        """ Variable name """
-
-        return self._name
-
-    @property
-    def program(self):
-        """ Program this variable belongs to """
-
-        return self._program
-
-    @property
-    def gtype(self):
-        """ Type of the underlying variable (as a GL constant) """
-
-        return self._gtype
-
-    @property
-    def dtype(self):
-        """ Equivalent dtype of the variable """
-
-        return self._dtype
-
-    @property
-    def enabled(self):
-        """ Whether this variable is being used by the program """
-        return self._enabled
-
-    @enabled.setter
-    def enabled(self, enabled):
-        """ Whether this variable is being used by the program """
-        self._enabled = bool(enabled)
-
-    @property
-    def data(self):
-        """ CPU data """
-        return self._data
-
-    def __repr__(self):
-        return "<%s %s>" % (self.__class__.__name__, self.name)
-
-
-# ----------------------------------------------------------- Uniform class ---
-class Uniform(Variable):
-    """ A Uniform represents a program uniform variable. 
-    
-    See Variable.
-    """
-
-    # todo: store function names instead of GL proxy function (faster)
-    _ufunctions = {
-        gl.GL_FLOAT:        gl.proxy.glUniform1fv,
-        gl.GL_FLOAT_VEC2:   gl.proxy.glUniform2fv,
-        gl.GL_FLOAT_VEC3:   gl.proxy.glUniform3fv,
-        gl.GL_FLOAT_VEC4:   gl.proxy.glUniform4fv,
-        gl.GL_INT:          gl.proxy.glUniform1iv,
-        gl.GL_BOOL:         gl.proxy.glUniform1iv,
-        gl.GL_FLOAT_MAT2:   gl.proxy.glUniformMatrix2fv,
-        gl.GL_FLOAT_MAT3:   gl.proxy.glUniformMatrix3fv,
-        gl.GL_FLOAT_MAT4:   gl.proxy.glUniformMatrix4fv,
-        #        gl.GL_SAMPLER_1D:   gl.proxy.glUniform1i,
-        gl.GL_SAMPLER_2D:   gl.proxy.glUniform1i,
-        GL_SAMPLER_3D:   gl.proxy.glUniform1i,
-    }
-
-    def __init__(self, program, name, gtype):
-        """ Initialize the input into default state """
-
-        Variable.__init__(self, program, name, gtype)
-        size, _, dtype = gl_typeinfo[self._gtype]
-        self._data = np.zeros(size, dtype)
-        self._ufunction = Uniform._ufunctions[self._gtype]
-        self._unit = -1
-        self._need_update = False
-
-    def set_data(self, data):
-        """ Set data (no upload) """
-        if self._gtype == gl.GL_SAMPLER_2D:
-            if isinstance(data, Texture2D):
-                self._data = data
-            elif isinstance(self._data, Texture2D):
-                self._data.set_data(data)
-            elif isinstance(data, RenderBuffer):
-                self._data = data
-            else:
-                # Automatic texture creation if required
-                data = np.array(data, copy=False)
-                if data.dtype in [np.float16, np.float32, np.float64]:
-                    self._data = Texture2D(data=data.astype(np.float32))
-                else:
-                    self._data = Texture2D(data=data.astype(np.uint8))
-        elif self._gtype == GL_SAMPLER_3D:
-            if isinstance(data, Texture3D):
-                self._data = data
-            elif isinstance(self._data, Texture3D):
-                self._data.set_data(data)
-            elif isinstance(data, RenderBuffer):
-                self._data = data
-            else:
-                # Automatic texture creation if required
-                data = np.array(data, copy=False)
-                if data.dtype in [np.float16, np.float32, np.float64]:
-                    self._data = Texture3D(data=data.astype(np.float32))
-                else:
-                    self._data = Texture3D(data=data.astype(np.uint8))
-        else:
-            self._data[...] = np.array(data, copy=False).ravel()
-
-        self._need_update = True
-
-    def _activate(self):
-        # if self._gtype in (gl.GL_SAMPLER_1D, gl.GL_SAMPLER_2D):
-        if self._gtype in (gl.GL_SAMPLER_2D, GL_SAMPLER_3D):
-            logger.debug("GPU: Active texture is %d" % self._unit)
-            gl.glActiveTexture(gl.GL_TEXTURE0 + self._unit)
-            if isinstance(self._data, BaseTexture):
-                self._data.activate()
-
-        # Update if necessary. OpenGL stores uniform values at the Program
-        # object, so they only have to be set once.
-        if self._need_update:
-            self._update()
-            self._need_update = False
-
-    def _deactivate(self):
-        if self._gtype in (gl.GL_SAMPLER_2D, GL_SAMPLER_3D):
-            #gl.glActiveTexture(gl.GL_TEXTURE0 + self._unit)
-            if self.data is not None:
-                self.data.deactivate()
-
-    def _update(self):
-
-        # Check active status (mandatory)
-        if not self._enabled:
-            raise RuntimeError("Uniform %r is not active" % self.name)
-        if self._data is None:
-            raise RuntimeError("Uniform data not set for %r" % self.name)
-        
-        # Matrices (need a transpose argument)
-        if self._gtype in (gl.GL_FLOAT_MAT2,
-                           gl.GL_FLOAT_MAT3, gl.GL_FLOAT_MAT4):
-            # OpenGL ES 2.0 does not support transpose
-            transpose = False
-            self._ufunction(self._handle, 1, transpose, self._data)
-
-        # Textures (need to get texture count)
-        # elif self._gtype in (gl.GL_SAMPLER_1D, gl.GL_SAMPLER_2D):
-        elif self._gtype in (gl.GL_SAMPLER_2D, GL_SAMPLER_3D):
-            # texture = self.data
-            # log("GPU: Active texture is %d" % self._unit)
-            # gl.glActiveTexture(gl.GL_TEXTURE0 + self._unit)
-            # gl.glBindTexture(texture.target, texture.handle)
-            gl.glUniform1i(self._handle, self._unit)
-
-        # Regular uniform
-        else:
-            self._ufunction(self._handle, 1, self._data)
-
-    def _create(self):
-        """ Create uniform on GPU (get handle) """
-        self._handle = gl.glGetUniformLocation(
-            self._program.handle, self._name)
-    
-    def _delete(self):
-        pass  # No need to delete variables; they are not really *objects*
-
-
-# --------------------------------------------------------- Attribute class ---
-class Attribute(Variable):
-    """ An Attribute represents a program attribute variable 
-    
-    See Variable.
-    """
-
-    _afunctions = {
-        gl.GL_FLOAT:      gl.proxy.glVertexAttrib1f,
-        gl.GL_FLOAT_VEC2: gl.proxy.glVertexAttrib2f,
-        gl.GL_FLOAT_VEC3: gl.proxy.glVertexAttrib3f,
-        gl.GL_FLOAT_VEC4: gl.proxy.glVertexAttrib4f
-    }
-
-    def __init__(self, program, name, gtype):
-        """ Initialize the input into default state """
-
-        Variable.__init__(self, program, name, gtype)
-
-        # Number of elements this attribute links to (in the attached buffer)
-        self._size = 0
-
-        # Whether this attribure is generic
-        self._generic = False
-
-    def set_data(self, data):
-        """ Set data (deferred operation) """
-        
-        isnumeric = isinstance(data, (float, int))
-        
-        if (isinstance(data, VertexBuffer) or
-            (isinstance(data, DataBufferView) and 
-             isinstance(data.base, VertexBuffer))):
-            # New vertex buffer
-            self._data = data
-        elif isinstance(self._data, VertexBuffer):
-            # We already have a vertex buffer
-            self._data.set_data(data)
-        elif (isnumeric or (isinstance(data, (tuple, list)) and
-                            len(data) in (1, 2, 3, 4) and
-                            isinstance(data[0], (float, int)))):
-            # Data is a tuple with size <= 4, we assume this designates
-            # a generate vertex attribute.
-            # Let numpy convert the data for us
-            _, _, dtype = gl_typeinfo[self._gtype]
-            self._data = np.array(data).astype(dtype)
-            self._generic = True
-            #self._need_update = True
-            self._afunction = Attribute._afunctions[self._gtype]
-            return
-        else:
-            # For array-like, we need to build a proper VertexBuffer
-            # to be able to upload it later to GPU memory.
-            name, base, count = self.dtype
-            data = np.array(data, dtype=base, copy=False)
-            data = data.ravel().view([self.dtype])
-            # WARNING : transform data with the right type
-            # data = np.array(data,copy=False)
-            self._data = VertexBuffer(data)
-        
-        self._generic = False
-
-    def _activate(self):
-        # Update always, attributes are not stored at the Program object
-        self._update()
-    
-    def _deactivate(self):
-        if isinstance(self.data, VertexBuffer):
-            self.data.deactivate()
-    
-    def _update(self):
-        """ Actual upload of data to GPU memory  """
-
-        #logger.debug("GPU: Updating %s" % self.name)
-        
-        # Check active status (mandatory)
-        if not self._enabled:
-            raise RuntimeError("Attribute %r is not active" % self.name)
-        if self._data is None:
-            raise RuntimeError("Attribute data not set for %r" % self.name)
-        
-        # Generic vertex attribute (all vertices receive the same value)
-        if self._generic:
-            if self._handle >= 0:
-                gl.glDisableVertexAttribArray(self._handle)
-                self._afunction(self._handle, *self._data)
-
-        # Regular vertex buffer
-        elif self._handle >= 0:
-            
-            # Activate the VBO
-            self.data.activate()
-            
-            # Get relevant information from gl_typeinfo
-            size, gtype, dtype = gl_typeinfo[self._gtype]
-            stride = self.data.stride
-            offset = self.data.offset
-
-            gl.glEnableVertexAttribArray(self.handle)
-            gl.glVertexAttribPointer(
-                self.handle, size, gtype, gl.GL_FALSE, stride, offset)
-
-    def _create(self):
-        """ Create attribute on GPU (get handle) """
-        self._handle = gl.glGetAttribLocation(self._program.handle, self.name)
-    
-    def _delete(self):
-        pass  # No need to delete variables; they are not really *objects*
-    
-    @property
-    def size(self):
-        """ Size of the underlying vertex buffer """
-
-        if self._data is None:
-            return 0
-        return self._data.size
diff --git a/vispy/gloo/wrappers.py b/vispy/gloo/wrappers.py
index 9f04e9b..10256db 100644
--- a/vispy/gloo/wrappers.py
+++ b/vispy/gloo/wrappers.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
@@ -11,20 +11,53 @@ from . import gl
 from ..ext.six import string_types
 from ..color import Color
 
+#from ..util import logger
+
+
+__all__ = ('set_viewport', 'set_depth_range', 'set_front_face',  # noqa
+           'set_cull_face', 'set_line_width', 'set_polygon_offset',  # noqa
+           'clear', 'set_clear_color', 'set_clear_depth', 'set_clear_stencil',  # noqa
+           'set_blend_func', 'set_blend_color', 'set_blend_equation',  # noqa
+           'set_scissor', 'set_stencil_func', 'set_stencil_mask',  # noqa
+           'set_stencil_op', 'set_depth_func', 'set_depth_mask',  # noqa
+           'set_color_mask', 'set_sample_coverage',  # noqa
+           'get_state_presets', 'set_state', 'finish', 'flush',  # noqa
+           'read_pixels', 'set_hint',  # noqa
+           'get_gl_configuration', '_check_valid',
+           'GlooFunctions', 'global_gloo_functions', )
 
-__all__ = ('set_viewport', 'set_depth_range', 'set_front_face',
-           'set_cull_face', 'set_line_width', 'set_polygon_offset',
-           'clear', 'set_clear_color', 'set_clear_depth', 'set_clear_stencil',
-           'set_blend_func', 'set_blend_color', 'set_blend_equation',
-           'set_scissor', 'set_stencil_func', 'set_stencil_mask',
-           'set_stencil_op', 'set_depth_func', 'set_depth_mask',
-           'set_color_mask', 'set_sample_coverage',
-           'get_state_presets', 'set_state', 'finish', 'flush',
-           'get_parameter', 'read_pixels', 'set_hint',
-           'get_gl_configuration', 'check_error', '_check_valid')
 _setters = [s[4:] for s in __all__
             if s.startswith('set_') and s != 'set_state']
 
+# NOTE: If these are updated to have things beyond glEnable/glBlendFunc
+# calls, set_preset_state will need to be updated to deal with it.
+_gl_presets = {
+    'opaque': dict(
+        depth_test=True, 
+        cull_face=False, 
+        blend=False),
+    'translucent': dict(
+        depth_test=True, 
+        cull_face=False, 
+        blend=True,
+        blend_func=('src_alpha', 'one_minus_src_alpha')),
+    'additive': dict(
+        depth_test=False, 
+        cull_face=False, 
+        blend=True,
+        blend_func=('src_alpha', 'one')),
+}
+    
+
+def get_current_canvas():
+    """ Proxy for context.get_current_canvas to avoud circular import.
+    This function replaces itself with the real function the first 
+    time it is called. (Bah)
+    """
+    from .context import get_current_canvas
+    globals()['get_current_canvas'] = get_current_canvas
+    return get_current_canvas()
+    
 
 # Helpers that are needed for efficient wrapping
 
@@ -35,21 +68,6 @@ def _check_valid(key, val, valid):
                          % (key, valid, val))
 
 
-def _gl_attr(x):
-    """Helper to return gl.GL_x enum"""
-    y = 'GL_' + x.upper()
-    z = getattr(gl, y, None)
-    if z is None:
-        raise ValueError('gl has no attribute corresponding to name %s (%s)'
-                         % (x, y))
-    return z
-
-
-def _gl_bool(x):
-    """Helper to convert to GL boolean"""
-    return gl.GL_TRUE if x else gl.GL_FALSE
-
-
 def _to_args(x):
     """Convert to args representation"""
     if not isinstance(x, (list, tuple, np.ndarray)):
@@ -61,507 +79,536 @@ def _check_conversion(key, valid_dict):
     """Check for existence of key in dict, return value or raise error"""
     if key not in valid_dict and key not in valid_dict.values():
         # Only show users the nice string values
-        keys = [v for v in valid_dict.keys() if isinstance(key, string_types)]
+        keys = [v for v in valid_dict.keys() if isinstance(v, string_types)]
         raise ValueError('value must be one of %s, not %s' % (keys, key))
     return valid_dict[key] if key in valid_dict else key
 
 
-###############################################################################
-# PRIMITIVE/VERTEX
-
-#
-# Viewport, DepthRangef, CullFace, FrontFace, LineWidth, PolygonOffset
-#
-
-def set_viewport(*args):
-    """Set the OpenGL viewport
-
-    This is a wrapper for gl.glViewport.
-
-    Parameters
-    ----------
-    x, y, w, h : int | tuple
-        X and Y coordinates, plus width and height. Can be passed in as
-        individual components, or as a single tuple with four values.
-    """
-    x, y, w, h = args[0] if len(args) == 1 else args
-    gl.glViewport(int(x), int(y), int(w), int(h))
-
-
-def set_depth_range(near=0., far=1.):
-    """Set depth values
-
-    Parameters
-    ----------
-    near : float
-        Near clipping plane.
-    far : float
-        Far clipping plane.
-    """
-    gl.glDepthRange(float(near), float(far))
-
-
-def set_front_face(mode='ccw'):
-    """Set which faces are front-facing
-
-    Parameters
-    ----------
-    mode : str
-        Can be 'cw' for clockwise or 'ccw' for counter-clockwise.
-    """
-    gl.glFrontFace(_gl_attr(mode))
-
-
-def set_cull_face(mode='back'):
-    """Set front, back, or both faces to be culled
-
-    Parameters
-    ----------
-    mode : str
-        Culling mode. Can be "front", "back", or "front_and_back".
-    """
-    gl.glCullFace(_gl_attr(mode))
-
-
-def set_line_width(width=1.):
-    """Set line width
-
-    Parameters
-    ----------
-    width : float
-        The line width.
-    """
-    gl.glLineWidth(float(width))
-
-
-def set_polygon_offset(factor=0., units=0.):
-    """Set the scale and units used to calculate depth values
-
-    Parameters
-    ----------
-    factor : float
-        Scale factor used to create a variable depth offset for each polygon.
-    units : float
-        Multiplied by an implementation-specific value to create a constant
-        depth offset.
+class BaseGlooFunctions(object):
+    """ Class that provides a series of GL functions that do not fit
+    in the object oriented part of gloo. An instance of this class is
+    associated with each canvas.
     """
-    gl.glPolygonOffset(float(factor), float(units))
-
-
-###############################################################################
-# FRAGMENT/SCREEN
-
-#
-# glClear, glClearColor, glClearDepthf, glClearStencil
-#
-
-def clear(color=True, depth=True, stencil=True):
-    """Clear the screen buffers
-
-    This is a wrapper for gl.glClear.
-
-    Parameters
-    ----------
-    color : bool | str | tuple | instance of Color
-        Clear the color buffer bit. If not bool, ``set_clear_color`` will
-        be used to set the color clear value.
-    depth : bool | float
-        Clear the depth buffer bit. If float, ``set_clear_depth`` will
-        be used to set the depth clear value.
-    stencil : bool | int
-        Clear the stencil buffer bit. If int, ``set_clear_stencil`` will
-        be used to set the stencil clear index.
-    """
-    bits = 0
-    if isinstance(color, np.ndarray) or bool(color):
-        if not isinstance(color, bool):
-            set_clear_color(color)
-        bits |= gl.GL_COLOR_BUFFER_BIT
-    if depth:
-        if not isinstance(depth, bool):
-            set_clear_depth(depth)
-        bits |= gl.GL_DEPTH_BUFFER_BIT
-    if stencil:
-        if not isinstance(stencil, bool):
-            set_clear_stencil(stencil)
-        bits |= gl.GL_STENCIL_BUFFER_BIT
-    gl.glClear(bits)
-
-
-def set_clear_color(color='black'):
-    """Set the screen clear color
-
-    This is a wrapper for gl.glClearColor.
-
-    Parameters
-    ----------
-    color : str | tuple | instance of Color
-        Color to use. See vispy.color.Color for options.
-    """
-    gl.glClearColor(*Color(color).rgba)
-
-
-def set_clear_depth(depth=1.0):
-    """Set the clear value for the depth buffer
-
-    This is a wrapper for gl.glClearDepth.
-
-    Parameters
-    ----------
-    depth : float
-        The depth to use.
-    """
-    gl.glClearDepth(float(depth))
-
-
-def set_clear_stencil(index=0):
-    """Set the clear value for the stencil buffer
-
-    This is a wrapper for gl.glClearStencil.
-
-    Parameters
-    ----------
-    index : int
-        The index to use when the stencil buffer is cleared.
-    """
-    gl.glClearStencil(int(index))
-
-
-# glBlendFunc(Separate), glBlendColor, glBlendEquation(Separate)
-
-def set_blend_func(srgb='one', drgb='zero',
-                   salpha=None, dalpha=None):
-    """Specify pixel arithmetic for RGB and alpha
-
-    Parameters
-    ----------
-    srgb : str
-        Source RGB factor.
-    drgb : str
-        Destination RGB factor.
-    salpha : str | None
-        Source alpha factor. If None, ``srgb`` is used.
-    dalpha : str
-        Destination alpha factor. If None, ``drgb`` is used.
-    """
-    salpha = srgb if salpha is None else salpha
-    dalpha = drgb if dalpha is None else dalpha
-    gl.glBlendFuncSeparate(_gl_attr(srgb), _gl_attr(drgb),
-                           _gl_attr(salpha), _gl_attr(dalpha))
-
-
-def set_blend_color(color):
-    """Set the blend color
-
-    Parameters
-    ----------
-    color : str | tuple | instance of Color
-        Color to use. See vispy.color.Color for options.
-    """
-    gl.glBlendColor(*Color(color).rgba)
-
-
-def set_blend_equation(mode_rgb, mode_alpha=None):
-    """Specify the equation for RGB and alpha blending
-
-    Parameters
-    ----------
-    mode_rgb : str
-        Mode for RGB.
-    mode_alpha : str | None
-        Mode for Alpha. If None, ``mode_rgb`` is used.
-
-    Notes
-    -----
-    See ``set_blend_equation`` for valid modes.
-    """
-    mode_alpha = mode_rgb if mode_alpha is None else mode_alpha
-    gl.glBlendEquationSeparate(_gl_attr(mode_rgb),
-                               _gl_attr(mode_alpha))
-
-
-# glScissor, glStencilFunc(Separate), glStencilMask(Separate),
-# glStencilOp(Separate),
-
-def set_scissor(x, y, w, h):
-    """Define the scissor box
-
-    Parameters
-    ----------
-    x : int
-        Left corner of the box.
-    y : int
-        Lower corner of the box.
-    w : int
-        The width of the box.
-    h : int
-        The height of the box.
-    """
-    gl.glScissor(int(x), int(y), int(w), int(h))
-
-
-def set_stencil_func(func='always', ref=0, mask=8, face='front_and_back'):
-    """Set front or back function and reference value
-
-    Parameters
-    ----------
-    func : str
-        See set_stencil_func.
-    ref : int
-        Reference value for the stencil test.
-    mask : int
-        Mask that is ANDed with ref and stored stencil value.
-    face : str
-        Can be 'front', 'back', or 'front_and_back'.
-    """
-    gl.glStencilFuncSeparate(_gl_attr(face), _gl_attr(func),
-                             int(ref), int(mask))
-
-
-def set_stencil_mask(mask=8, face='front_and_back'):
-    """Control the front or back writing of individual bits in the stencil
-
-    Parameters
-    ----------
-    mask : int
-        Mask that is ANDed with ref and stored stencil value.
-    face : str
-        Can be 'front', 'back', or 'front_and_back'.
-    """
-    gl.glStencilMaskSeparate(_gl_attr(face), int(mask))
-
-
-def set_stencil_op(sfail='keep', dpfail='keep', dppass='keep',
-                   face='front_and_back'):
-    """Set front or back stencil test actions
-
-    Parameters
-    ----------
-    sfail : str
-        Action to take when the stencil fails. Must be one of
-        'keep', 'zero', 'replace', 'incr', 'incr_wrap',
-        'decr', 'decr_wrap', or 'invert'.
-    dpfail : str
-        Action to take when the stencil passes.
-    dppass : str
-        Action to take when both the stencil and depth tests pass,
-        or when the stencil test passes and either there is no depth
-        buffer or depth testing is not enabled.
-    face : str
-        Can be 'front', 'back', or 'front_and_back'.
-    """
-    gl.glStencilOpSeparate(_gl_attr(face), _gl_attr(sfail),
-                           _gl_attr(dpfail), _gl_attr(dppass))
-
-
-# glDepthFunc, glDepthMask, glColorMask, glSampleCoverage
-
-def set_depth_func(func='less'):
-    """Specify the value used for depth buffer comparisons
-
-    Parameters
-    ----------
-    func : str
-        The depth comparison function. Must be one of 'never', 'less', 'equal',
-        'lequal', 'greater', 'gequal', 'notequal', or 'always'.
-    """
-    gl.glDepthFunc(_gl_attr(func))
-
-
-def set_depth_mask(flag):
-    """Toggle writing into the depth buffer
-
-    Parameters
-    ----------
-    flag : bool
-        Whether depth writing should be enabled.
-    """
-    gl.glDepthMask(_gl_bool(flag))
-
-
-def set_color_mask(red, green, blue, alpha):
-    """Toggle writing of frame buffer color components
-
-    Parameters
-    ----------
-    red : bool
-        Red toggle.
-    green : bool
-        Green toggle.
-    blue : bool
-        Blue toggle.
-    alpha : bool
-        Alpha toggle.
-    """
-    gl.glColorMask(_gl_bool(red), _gl_bool(green), _gl_bool(blue),
-                   _gl_bool(alpha))
-
-
-def set_sample_coverage(value=1.0, invert=False):
-    """Specify multisample coverage parameters
-
-    Parameters
-    ----------
-    value : float
-        Sample coverage value (will be clamped between 0. and 1.).
-    invert : bool
-        Specify if the coverage masks should be inverted.
-    """
-    gl.glSampleCoverage(float(value), _gl_bool(invert))
-
-
-###############################################################################
-# STATE
-
-#
-# glEnable/Disable
-#
-
-# NOTE: If these are updated to have things beyond glEnable/glBlendFunc
-# calls, set_preset_state will need to be updated to deal with it.
-_gl_presets = dict(
-    opaque=dict(depth_test=True, cull_face=False, blend=False),
-    translucent=dict(depth_test=True, cull_face=False, blend=True,
-                     blend_func=('src_alpha', 'one_minus_src_alpha')),
-    additive=dict(depth_test=False, cull_face=False, blend=True,
-                  blend_func=('src_alpha', 'one'),)
-)
-
-
-def get_state_presets():
-    """The available GL state presets
-
-    Returns
-    -------
-    presets : dict
-        The dictionary of presets usable with ``set_options``.
-    """
-    return deepcopy(_gl_presets)
-
-
-_known_state_names = ('depth_test', 'blend', 'blend_func')
-
-
-def set_state(preset=None, **kwargs):
-    """Set OpenGL rendering state, optionally using a preset
-
-    Parameters
-    ----------
-    preset : str | None
-        Can be one of ('opaque', 'translucent', 'additive') to use
-        use reasonable defaults for these typical use cases.
-    **kwargs : keyword arguments
-        Other supplied keyword arguments will override any preset defaults.
-        Options to be enabled or disabled should be supplied as booleans
-        (e.g., ``'depth_test=True'``, ``cull_face=False``), non-boolean
-        entries will be passed as arguments to ``set_*`` functions (e.g.,
-        ``blend_func=('src_alpha', 'one')`` will call ``set_blend_func``).
-
-    Notes
-    -----
-    This serves three purposes:
-
-      1. Set GL state using reasonable presets.
-      2. Wrapping glEnable/glDisable functionality.
-      3. Convienence wrapping of other ``gloo.set_*`` functions.
-
-    For example, one could do the following:
-
-        >>> from vispy import gloo
-        >>> gloo.set_state('translucent', depth_test=False, clear_color=(1, 1, 1, 1))  # noqa, doctest:+SKIP
-
-    This would take the preset defaults for 'translucent', turn depth testing
-    off (which would normally be on for that preset), and additionally
-    set the glClearColor parameter to be white.
-
-    Another example to showcase glEnable/glDisable wrapping:
-
-        >>> gloo.set_state(blend=True, depth_test=True, polygon_offset_fill=False)  # noqa, doctest:+SKIP
-
-    This would be equivalent to calling
-
-        >>> from vispy.gloo import gl
-        >>> gl.glDisable(gl.GL_BLEND)
-        >>> gl.glEnable(gl.GL_DEPTH_TEST)
-        >>> gl.glEnable(gl.GL_POLYGON_OFFSET_FILL)
-
-    Or here's another example:
-
-        >>> gloo.set_state(clear_color=(0, 0, 0, 1), blend=True, blend_func=('src_alpha', 'one'))  # noqa, doctest:+SKIP
-
-    Thus arbitrary GL state components can be set directly using ``set_state``.
-    Note that individual functions are exposed e.g., as ``set_clear_color``,
-    with some more informative docstrings about those particular functions.
-    """
-    kwargs = deepcopy(kwargs)
-    # Load preset, if supplied
-    if preset is not None:
-        _check_valid('preset', preset, tuple(list(_gl_presets.keys())))
-        for key, val in _gl_presets[preset].items():
-            # only overwrite user's input with preset if user's input is None
-            if key not in kwargs:
-                kwargs[key] = val
-
-    # cull_face is an exception because GL_CULL_FACE and glCullFace both exist
-    if 'cull_face' in kwargs:
-        cull_face = kwargs.pop('cull_face')
-        if isinstance(cull_face, bool):
-            func = gl.glEnable if cull_face else gl.glDisable
-            func(_gl_attr('cull_face'))
+    
+    ##########################################################################
+    # PRIMITIVE/VERTEX
+    
+    #
+    # Viewport, DepthRangef, CullFace, FrontFace, LineWidth, PolygonOffset
+    #
+    
+    def set_viewport(self, *args):
+        """Set the OpenGL viewport
+    
+        This is a wrapper for gl.glViewport.
+    
+        Parameters
+        ----------
+        *args : tuple
+            X and Y coordinates, plus width and height. Can be passed in as
+            individual components, or as a single tuple with four values.
+        """
+        x, y, w, h = args[0] if len(args) == 1 else args
+        self.glir.command('FUNC', 'glViewport', int(x), int(y), int(w), int(h))
+    
+    def set_depth_range(self, near=0., far=1.):
+        """Set depth values
+    
+        Parameters
+        ----------
+        near : float
+            Near clipping plane.
+        far : float
+            Far clipping plane.
+        """
+        self.glir.command('FUNC', 'glDepthRange', float(near), float(far))
+    
+    def set_front_face(self, mode='ccw'):
+        """Set which faces are front-facing
+    
+        Parameters
+        ----------
+        mode : str
+            Can be 'cw' for clockwise or 'ccw' for counter-clockwise.
+        """
+        self.glir.command('FUNC', 'glFrontFace', mode)
+    
+    def set_cull_face(self, mode='back'):
+        """Set front, back, or both faces to be culled
+    
+        Parameters
+        ----------
+        mode : str
+            Culling mode. Can be "front", "back", or "front_and_back".
+        """
+        self.glir.command('FUNC', 'glCullFace', mode)
+    
+    def set_line_width(self, width=1.):
+        """Set line width
+    
+        Parameters
+        ----------
+        width : float
+            The line width.
+        """
+        width = float(width)
+        if width < 0:
+            raise RuntimeError('Cannot have width < 0')
+        self.glir.command('FUNC', 'glLineWidth', width)
+    
+    def set_polygon_offset(self, factor=0., units=0.):
+        """Set the scale and units used to calculate depth values
+    
+        Parameters
+        ----------
+        factor : float
+            Scale factor used to create a variable depth offset for
+            each polygon.
+        units : float
+            Multiplied by an implementation-specific value to create a
+            constant depth offset.
+        """
+        self.glir.command('FUNC', 'glPolygonOffset', float(factor),
+                          float(units))
+    
+    ##########################################################################
+    # FRAGMENT/SCREEN
+    
+    #
+    # glClear, glClearColor, glClearDepthf, glClearStencil
+    #
+    
+    def clear(self, color=True, depth=True, stencil=True):
+        """Clear the screen buffers
+    
+        This is a wrapper for gl.glClear.
+    
+        Parameters
+        ----------
+        color : bool | str | tuple | instance of Color
+            Clear the color buffer bit. If not bool, ``set_clear_color`` will
+            be used to set the color clear value.
+        depth : bool | float
+            Clear the depth buffer bit. If float, ``set_clear_depth`` will
+            be used to set the depth clear value.
+        stencil : bool | int
+            Clear the stencil buffer bit. If int, ``set_clear_stencil`` will
+            be used to set the stencil clear index.
+        """
+        bits = 0
+        if isinstance(color, np.ndarray) or bool(color):
+            if not isinstance(color, bool):
+                self.set_clear_color(color)
+            bits |= gl.GL_COLOR_BUFFER_BIT
+        if depth:
+            if not isinstance(depth, bool):
+                self.set_clear_depth(depth)
+            bits |= gl.GL_DEPTH_BUFFER_BIT
+        if stencil:
+            if not isinstance(stencil, bool):
+                self.set_clear_stencil(stencil)
+            bits |= gl.GL_STENCIL_BUFFER_BIT
+        self.glir.command('FUNC', 'glClear', bits)
+
+    def set_clear_color(self, color='black', alpha=None):
+        """Set the screen clear color
+
+        This is a wrapper for gl.glClearColor.
+
+        Parameters
+        ----------
+        color : str | tuple | instance of Color
+            Color to use. See vispy.color.Color for options.
+        alpha : float | None
+            Alpha to use.
+        """
+        self.glir.command('FUNC', 'glClearColor', *Color(color, alpha).rgba)
+
+    def set_clear_depth(self, depth=1.0):
+        """Set the clear value for the depth buffer
+
+        This is a wrapper for gl.glClearDepth.
+
+        Parameters
+        ----------
+        depth : float
+            The depth to use.
+        """
+        self.glir.command('FUNC', 'glClearDepth', float(depth))
+    
+    def set_clear_stencil(self, index=0):
+        """Set the clear value for the stencil buffer
+    
+        This is a wrapper for gl.glClearStencil.
+    
+        Parameters
+        ----------
+        index : int
+            The index to use when the stencil buffer is cleared.
+        """
+        self.glir.command('FUNC', 'glClearStencil', int(index))
+    
+    # glBlendFunc(Separate), glBlendColor, glBlendEquation(Separate)
+    
+    def set_blend_func(self, srgb='one', drgb='zero',
+                       salpha=None, dalpha=None):
+        """Specify pixel arithmetic for RGB and alpha
+    
+        Parameters
+        ----------
+        srgb : str
+            Source RGB factor.
+        drgb : str
+            Destination RGB factor.
+        salpha : str | None
+            Source alpha factor. If None, ``srgb`` is used.
+        dalpha : str
+            Destination alpha factor. If None, ``drgb`` is used.
+        """
+        salpha = srgb if salpha is None else salpha
+        dalpha = drgb if dalpha is None else dalpha
+        self.glir.command('FUNC', 'glBlendFuncSeparate', 
+                          srgb, drgb, salpha, dalpha)
+    
+    def set_blend_color(self, color):
+        """Set the blend color
+    
+        Parameters
+        ----------
+        color : str | tuple | instance of Color
+            Color to use. See vispy.color.Color for options.
+        """
+        self.glir.command('FUNC', 'glBlendColor', *Color(color).rgba)
+    
+    def set_blend_equation(self, mode_rgb, mode_alpha=None):
+        """Specify the equation for RGB and alpha blending
+    
+        Parameters
+        ----------
+        mode_rgb : str
+            Mode for RGB.
+        mode_alpha : str | None
+            Mode for Alpha. If None, ``mode_rgb`` is used.
+    
+        Notes
+        -----
+        See ``set_blend_equation`` for valid modes.
+        """
+        mode_alpha = mode_rgb if mode_alpha is None else mode_alpha
+        self.glir.command('FUNC', 'glBlendEquationSeparate', 
+                          mode_rgb, mode_alpha)
+    
+    # glScissor, glStencilFunc(Separate), glStencilMask(Separate),
+    # glStencilOp(Separate),
+    
+    def set_scissor(self, x, y, w, h):
+        """Define the scissor box
+    
+        Parameters
+        ----------
+        x : int
+            Left corner of the box.
+        y : int
+            Lower corner of the box.
+        w : int
+            The width of the box.
+        h : int
+            The height of the box.
+        """
+        self.glir.command('FUNC', 'glScissor', int(x), int(y), int(w), int(h))
+    
+    def set_stencil_func(self, func='always', ref=0, mask=8, 
+                         face='front_and_back'):
+        """Set front or back function and reference value
+    
+        Parameters
+        ----------
+        func : str
+            See set_stencil_func.
+        ref : int
+            Reference value for the stencil test.
+        mask : int
+            Mask that is ANDed with ref and stored stencil value.
+        face : str
+            Can be 'front', 'back', or 'front_and_back'.
+        """
+        self.glir.command('FUNC', 'glStencilFuncSeparate', 
+                          face, func, int(ref), int(mask))
+    
+    def set_stencil_mask(self, mask=8, face='front_and_back'):
+        """Control the front or back writing of individual bits in the stencil
+    
+        Parameters
+        ----------
+        mask : int
+            Mask that is ANDed with ref and stored stencil value.
+        face : str
+            Can be 'front', 'back', or 'front_and_back'.
+        """
+        self.glir.command('FUNC', 'glStencilMaskSeparate', face, int(mask))
+    
+    def set_stencil_op(self, sfail='keep', dpfail='keep', dppass='keep',
+                       face='front_and_back'):
+        """Set front or back stencil test actions
+    
+        Parameters
+        ----------
+        sfail : str
+            Action to take when the stencil fails. Must be one of
+            'keep', 'zero', 'replace', 'incr', 'incr_wrap',
+            'decr', 'decr_wrap', or 'invert'.
+        dpfail : str
+            Action to take when the stencil passes.
+        dppass : str
+            Action to take when both the stencil and depth tests pass,
+            or when the stencil test passes and either there is no depth
+            buffer or depth testing is not enabled.
+        face : str
+            Can be 'front', 'back', or 'front_and_back'.
+        """
+        self.glir.command('FUNC', 'glStencilOpSeparate', 
+                          face, sfail, dpfail, dppass)
+    
+    # glDepthFunc, glDepthMask, glColorMask, glSampleCoverage
+    
+    def set_depth_func(self, func='less'):
+        """Specify the value used for depth buffer comparisons
+    
+        Parameters
+        ----------
+        func : str
+            The depth comparison function. Must be one of 'never', 'less', 
+            'equal', 'lequal', 'greater', 'gequal', 'notequal', or 'always'.
+        """
+        self.glir.command('FUNC', 'glDepthFunc', func)
+    
+    def set_depth_mask(self, flag):
+        """Toggle writing into the depth buffer
+    
+        Parameters
+        ----------
+        flag : bool
+            Whether depth writing should be enabled.
+        """
+        self.glir.command('FUNC', 'glDepthMask', bool(flag))
+    
+    def set_color_mask(self, red, green, blue, alpha):
+        """Toggle writing of frame buffer color components
+    
+        Parameters
+        ----------
+        red : bool
+            Red toggle.
+        green : bool
+            Green toggle.
+        blue : bool
+            Blue toggle.
+        alpha : bool
+            Alpha toggle.
+        """
+        self.glir.command('FUNC', 'glColorMask', bool(red), bool(green), 
+                          bool(blue), bool(alpha))
+    
+    def set_sample_coverage(self, value=1.0, invert=False):
+        """Specify multisample coverage parameters
+    
+        Parameters
+        ----------
+        value : float
+            Sample coverage value (will be clamped between 0. and 1.).
+        invert : bool
+            Specify if the coverage masks should be inverted.
+        """
+        self.glir.command('FUNC', 'glSampleCoverage', float(value), 
+                          bool(invert))
+    
+    ##########################################################################
+    # STATE
+    
+    #
+    # glEnable/Disable
+    #
+    
+    def get_state_presets(self):
+        """The available GL state presets
+    
+        Returns
+        -------
+        presets : dict
+            The dictionary of presets usable with ``set_options``.
+        """
+        return deepcopy(_gl_presets)
+    
+    def set_state(self, preset=None, **kwargs):
+        """Set OpenGL rendering state, optionally using a preset
+    
+        Parameters
+        ----------
+        preset : str | None
+            Can be one of ('opaque', 'translucent', 'additive') to use
+            use reasonable defaults for these typical use cases.
+        **kwargs : keyword arguments
+            Other supplied keyword arguments will override any preset defaults.
+            Options to be enabled or disabled should be supplied as booleans
+            (e.g., ``'depth_test=True'``, ``cull_face=False``), non-boolean
+            entries will be passed as arguments to ``set_*`` functions (e.g.,
+            ``blend_func=('src_alpha', 'one')`` will call ``set_blend_func``).
+    
+        Notes
+        -----
+        This serves three purposes:
+    
+        1. Set GL state using reasonable presets.
+        2. Wrapping glEnable/glDisable functionality.
+        3. Convienence wrapping of other ``gloo.set_*`` functions.
+    
+        For example, one could do the following:
+    
+            >>> from vispy import gloo
+            >>> gloo.set_state('translucent', depth_test=False, clear_color=(1, 1, 1, 1))  # noqa, doctest:+SKIP
+    
+        This would take the preset defaults for 'translucent', turn
+        depth testing off (which would normally be on for that preset),
+        and additionally set the glClearColor parameter to be white.
+    
+        Another example to showcase glEnable/glDisable wrapping:
+    
+            >>> gloo.set_state(blend=True, depth_test=True, polygon_offset_fill=False)  # noqa, doctest:+SKIP
+    
+        This would be equivalent to calling
+    
+            >>> from vispy.gloo import gl
+            >>> gl.glDisable(gl.GL_BLEND)
+            >>> gl.glEnable(gl.GL_DEPTH_TEST)
+            >>> gl.glEnable(gl.GL_POLYGON_OFFSET_FILL)
+    
+        Or here's another example:
+    
+            >>> gloo.set_state(clear_color=(0, 0, 0, 1), blend=True, blend_func=('src_alpha', 'one'))  # noqa, doctest:+SKIP
+    
+        Thus arbitrary GL state components can be set directly using
+        ``set_state``. Note that individual functions are exposed e.g.,
+        as ``set_clear_color``, with some more informative docstrings
+        about those particular functions.
+        """
+        kwargs = deepcopy(kwargs)
+        
+        # Load preset, if supplied
+        if preset is not None:
+            _check_valid('preset', preset, tuple(list(_gl_presets.keys())))
+            for key, val in _gl_presets[preset].items():
+                # only overwrite user input with preset if user's input is None
+                if key not in kwargs:
+                    kwargs[key] = val
+    
+        # cull_face is an exception because GL_CULL_FACE, glCullFace both exist
+        if 'cull_face' in kwargs:
+            cull_face = kwargs.pop('cull_face')
+            if isinstance(cull_face, bool):
+                funcname = 'glEnable' if cull_face else 'glDisable'
+                self.glir.command('FUNC', funcname, 'cull_face')
+            else:
+                self.glir.command('FUNC', 'glEnable', 'cull_face')
+                self.set_cull_face(*_to_args(cull_face))
+        
+        # Iterate over kwargs
+        for key, val in kwargs.items():
+            if key in _setters:
+                # Setter
+                args = _to_args(val)
+                # these actually need tuples
+                if key in ('blend_color', 'clear_color') and \
+                        not isinstance(args[0], string_types):
+                    args = [args]
+                getattr(self, 'set_' + key)(*args)
+            else:
+                # Enable / disable
+                funcname = 'glEnable' if val else 'glDisable'
+                self.glir.command('FUNC', funcname, key)
+    
+    #
+    # glFinish, glFlush, glReadPixels, glHint
+    #
+    
+    def finish(self):
+        """Wait for GL commands to to finish
+        
+        This creates a GLIR command for glFinish and then processes the
+        GLIR commands. If the GLIR interpreter is remote (e.g. WebGL), this
+        function will return before GL has finished processing the commands.
+        """
+        if hasattr(self, 'flush_commands'):
+            context = self
         else:
-            set_cull_face(*_to_args(cull_face))
-
-    # Now deal with other non-glEnable/glDisable args
-    for s in _setters:
-        if s in kwargs:
-            args = _to_args(kwargs.pop(s))
-            # these actually need tuples
-            if s in ('blend_color', 'clear_color') and \
-                    not isinstance(args[0], string_types):
-                args = [args]
-            globals()['set_' + s](*args)
-
-    # check values and translate
-    for key, val in kwargs.items():
-        func = gl.glEnable if val else gl.glDisable
-        func(_gl_attr(key))
-
-
-#
-# glFinish, glFlush, glGetParameter, glReadPixels, glHint
-#
-
-def finish():
-    """Wait for GL commands to to finish
-
-    This is a wrapper for glFinish().
-    """
-    gl.glFinish()
-
-
-def flush():
-    """Flush GL commands
-
-    This is a wrapper for glFlush().
-    """
-    gl.glFlush()
-
-
-def get_parameter(name):
-    """Get OpenGL parameter value
-
-    Parameters
-    ----------
-    name : str
-        The name of the parameter to get.
-    """
-    if not isinstance(name, string_types):
-        raise TypeError('name bust be a string')
-    return gl.glGetParameter(_gl_attr(name))
-
-
-def read_pixels(viewport=None, alpha=True):
-    """Read pixels from the front buffer
+            context = get_current_canvas().context
+        context.glir.command('FUNC', 'glFinish')
+        context.flush_commands()  # Process GLIR commands
+    
+    def flush(self):
+        """Flush GL commands
+    
+        This is a wrapper for glFlush(). This also flushes the GLIR
+        command queue.
+        """
+        if hasattr(self, 'flush_commands'):
+            context = self
+        else:
+            context = get_current_canvas().context
+        context.glir.command('FUNC', 'glFlush')
+        context.flush_commands()  # Process GLIR commands
+    
+    def set_hint(self, target, mode):
+        """Set OpenGL drawing hint
+    
+        Parameters
+        ----------
+        target : str
+            The target, e.g. 'fog_hint', 'line_smooth_hint',
+            'point_smooth_hint'.
+        mode : str
+            The mode to set (e.g., 'fastest', 'nicest', 'dont_care').
+        """
+        if not all(isinstance(tm, string_types) for tm in (target, mode)):
+            raise TypeError('target and mode must both be strings')
+        self.glir.command('FUNC', 'glHint', target, mode)
+
+
+class GlooFunctions(BaseGlooFunctions):
+    
+    @property
+    def glir(self):
+        """ The GLIR queue corresponding to the current canvas
+        """
+        canvas = get_current_canvas()
+        if canvas is None:
+            msg = ("If you want to use gloo without vispy.app, " + 
+                   "use a gloo.context.FakeCanvas.")
+            raise RuntimeError('Gloo requires a Canvas to run.\n' + msg)
+        return canvas.context.glir
+
+
+## Create global functions object and inject names here
+
+# GlooFunctions without queue: use queue of canvas that is current at call-time
+global_gloo_functions = GlooFunctions() 
+
+for name in dir(global_gloo_functions):
+    if name.startswith('_') or name in ('glir'):
+        continue
+    fun = getattr(global_gloo_functions, name)
+    if callable(fun):
+        globals()[name] = fun
+
+
+## Functions that do not use the glir queue
+
+
+def read_pixels(viewport=None, alpha=True, out_type='unsigned_byte'):
+    """Read pixels from the currently selected buffer. 
+    
+    Under most circumstances, this function reads from the front buffer.
+    Unlike all other functions in vispy.gloo, this function directly executes
+    an OpenGL command.
 
     Parameters
     ----------
@@ -570,56 +617,52 @@ def read_pixels(viewport=None, alpha=True):
         the current GL viewport will be queried and used.
     alpha : bool
         If True (default), the returned array has 4 elements (RGBA).
-        Otherwise, it has 3 (RGB).
+        If False, it has 3 (RGB).
+    out_type : str | dtype
+        Can be 'unsigned_byte' or 'float'. Note that this does not
+        use casting, but instead determines how values are read from
+        the current buffer. Can also be numpy dtypes ``np.uint8``,
+        ``np.ubyte``, or ``np.float32``.
 
     Returns
     -------
     pixels : array
-        3D array of pixels in np.uint8 format.
+        3D array of pixels in np.uint8 or np.float32 format. 
+        The array shape is (h, w, 3) or (h, w, 4), with the top-left corner 
+        of the framebuffer at index [0, 0] in the returned array.
     """
+    # Check whether the GL context is direct or remote
+    context = get_current_canvas().context
+    if context.shared.parser.is_remote():
+        raise RuntimeError('Cannot use read_pixels() with remote GLIR parser')
+    
+    finish()  # noqa - finish first, also flushes GLIR commands
+    type_dict = {'unsigned_byte': gl.GL_UNSIGNED_BYTE,
+                 np.uint8: gl.GL_UNSIGNED_BYTE,
+                 'float': gl.GL_FLOAT,
+                 np.float32: gl.GL_FLOAT}
+    type_ = _check_conversion(out_type, type_dict)
     if viewport is None:
-        viewport = get_parameter('viewport')
+        viewport = gl.glGetParameter(gl.GL_VIEWPORT)
     viewport = np.array(viewport, int)
     if viewport.ndim != 1 or viewport.size != 4:
         raise ValueError('viewport should be 1D 4-element array-like, not %s'
                          % (viewport,))
     x, y, w, h = viewport
     gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)  # PACK, not UNPACK
-    if alpha:  # gl.GL_RGBA
-        im = gl.glReadPixels(x, y, w, h, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE)
-    else:  # gl.gl_RGB
-        im = gl.glReadPixels(x, y, w, h, gl.GL_RGB, gl.GL_UNSIGNED_BYTE)
+    fmt = gl.GL_RGBA if alpha else gl.GL_RGB
+    im = gl.glReadPixels(x, y, w, h, fmt, type_)
     gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 4)
     # reshape, flip, and return
     if not isinstance(im, np.ndarray):
-        im = np.frombuffer(im, np.uint8)
+        np_dtype = np.uint8 if type_ == gl.GL_UNSIGNED_BYTE else np.float32
+        im = np.frombuffer(im, np_dtype)
 
-    if alpha:
-        im.shape = h, w, 4  # RGBA
-    else:
-        im.shape = h, w, 3  # RGB
+    im.shape = h, w, (4 if alpha else 3)  # RGBA vs RGB
     im = im[::-1, :, :]  # flip the image
     return im
 
 
-def set_hint(target, mode):
-    """Set OpenGL drawing hint
-
-    Parameters
-    ----------
-    target : str
-        The target (e.g., 'fog_hint', 'line_smooth_hint', 'point_smooth_hint').
-    mode : str
-        The mode to set (e.g., 'fastest', 'nicest', 'dont_care').
-    """
-    if not all(isinstance(tm, string_types) for tm in (target, mode)):
-        raise TypeError('target and mode must both be strings')
-    gl.glHint(_gl_attr(target), _gl_attr(mode))
-
-
-###############################################################################
-# Current OpenGL configuration
-
 def get_gl_configuration():
     """Read the current gl configuration
 
@@ -663,12 +706,3 @@ def get_gl_configuration():
     config['samples'] = gl.glGetParameter(gl.GL_SAMPLES)
     gl.check_error('post-config check')
     return config
-
-
-def check_error():
-    """Check for OpenGL errors
-
-    For efficiency, errors are only checked periodically. This forces
-    a check for OpenGL errors.
-    """
-    gl.check_error('gloo check')
diff --git a/vispy/glsl/__init__.py b/vispy/glsl/__init__.py
new file mode 100644
index 0000000..6de5ac3
--- /dev/null
+++ b/vispy/glsl/__init__.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+import os
+import os.path as op
+
+from vispy import config
+
+
+def find(name):
+    """Locate a filename into the shader library."""
+
+    if op.exists(name):
+        return name
+
+    path = op.dirname(__file__) or '.'
+
+    paths = [path] + config['include_path']
+
+    for path in paths:
+        filename = op.abspath(op.join(path, name))
+        if op.exists(filename):
+            return filename
+
+        for d in os.listdir(path):
+            fullpath = op.abspath(op.join(path, d))
+            if op.isdir(fullpath):
+                filename = op.abspath(op.join(fullpath, name))
+                if op.exists(filename):
+                    return filename
+
+    return None
+
+
+def get(name):
+    """Retrieve code from the given filename."""
+
+    filename = find(name)
+    if filename is None:
+        raise RuntimeError('Could not find %s' % name)
+    with open(filename) as fid:
+        return fid.read()
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/glsl/antialias/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/glsl/antialias/__init__.py
diff --git a/vispy/glsl/antialias/antialias.glsl b/vispy/glsl/antialias/antialias.glsl
new file mode 100644
index 0000000..2776363
--- /dev/null
+++ b/vispy/glsl/antialias/antialias.glsl
@@ -0,0 +1,7 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/stroke.glsl"
+#include "antialias/filled.glsl"
+#include "antialias/outline.glsl"
diff --git a/vispy/glsl/antialias/cap-butt.glsl b/vispy/glsl/antialias/cap-butt.glsl
new file mode 100644
index 0000000..fcb5cdb
--- /dev/null
+++ b/vispy/glsl/antialias/cap-butt.glsl
@@ -0,0 +1,31 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/stroke.glsl"
+
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for a line cap.
+   Type: butt
+
+   Parameters:
+   -----------
+
+
+   dx,dy    : signed distances to cap point (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   stroke:    Stroke color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+vec4 cap_butt(float dx, float dy, float linewidth, float antialias, vec4 color)
+{
+    float t = linewidth/2.0 - antialias;
+    float d = max(abs(dx)+t, abs(dy));
+    return stroke(d, linewidth, antialias, color);
+}
diff --git a/vispy/glsl/antialias/cap-round.glsl b/vispy/glsl/antialias/cap-round.glsl
new file mode 100644
index 0000000..4fd65e7
--- /dev/null
+++ b/vispy/glsl/antialias/cap-round.glsl
@@ -0,0 +1,29 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/stroke.glsl"
+
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for a line cap.
+   Type: round
+
+   Parameters:
+   -----------
+
+   dx,dy    : signed distances to cap point (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   stroke:    Stroke color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+vec4 cap_round(float dx, float dy, float linewidth, float antialias, vec4 color)
+{
+    float d = lenght(vec2(dx,dy));
+    return stroke(d, linewidth, antialias, color);
+}
diff --git a/vispy/glsl/antialias/cap-square.glsl b/vispy/glsl/antialias/cap-square.glsl
new file mode 100644
index 0000000..4b0cd5d
--- /dev/null
+++ b/vispy/glsl/antialias/cap-square.glsl
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/stroke.glsl"
+
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for a line cap.
+   Type: square
+
+   Parameters:
+   -----------
+
+   dx,dy    : signed distances to cap point (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   stroke:    Stroke color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+vec4 cap_square(float dx, float dy, float linewidth, float antialias, vec4 color)
+{
+    float t = linewidth/2.0 - antialias;
+    float d = max(abs(dx),abs(dy));
+    return stroke(d, linewidth, antialias, color);
+}
diff --git a/vispy/glsl/antialias/cap-triangle-in.glsl b/vispy/glsl/antialias/cap-triangle-in.glsl
new file mode 100644
index 0000000..0b6133a
--- /dev/null
+++ b/vispy/glsl/antialias/cap-triangle-in.glsl
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/stroke.glsl"
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for a line cap.
+   Type: triangle in
+
+   Parameters:
+   -----------
+
+   type     : Type of cap
+   dx,dy    : signed distances to cap point (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   stroke:    Stroke color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+vec4 cap_triangle_in(float dx, float dy, float linewidth, float antialias, vec4 color)
+{
+    float t = linewidth/2.0 - antialias;
+    float d = (abs(dx)+abs(dy));
+    return stroke(d, linewidth, antialias, color);
+}
diff --git a/vispy/glsl/antialias/cap-triangle-out.glsl b/vispy/glsl/antialias/cap-triangle-out.glsl
new file mode 100644
index 0000000..ce25952
--- /dev/null
+++ b/vispy/glsl/antialias/cap-triangle-out.glsl
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/stroke.glsl"
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for a line cap.
+   Type: triangle out
+
+   Parameters:
+   -----------
+
+   dx,dy    : signed distances to cap point (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   stroke:    Stroke color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+vec4 cap_triangle_out(float dx, float dy, float linewidth, float antialias, vec4 color)
+{
+    float t = linewidth/2.0 - antialias;
+    float d = max(abs(dy),(t+abs(dx)-abs(dy)));
+
+    return stroke(d, linewidth, antialias, color);
+}
diff --git a/vispy/glsl/antialias/cap.glsl b/vispy/glsl/antialias/cap.glsl
new file mode 100644
index 0000000..b5241af
--- /dev/null
+++ b/vispy/glsl/antialias/cap.glsl
@@ -0,0 +1,67 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/stroke.glsl"
+
+// Cap types
+// ----------------------------
+const int CAP_NONE         = 0;
+const int CAP_ROUND        = 1;
+const int CAP_TRIANGLE_IN  = 2;
+const int CAP_TRIANGLE_OUT = 3;
+const int CAP_SQUARE       = 4;
+const int CAP_BUTT         = 5;
+
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for a line cap.
+   Require the stroke function.
+
+   Parameters:
+   -----------
+
+   type     : Type of cap
+   dx,dy    : signed distances to cap point (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   stroke:    Stroke color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+vec4 cap(int type, float dx, float dy, float linewidth, float antialias, vec4 color)
+{
+    float d = 0.0;
+    dx = abs(dx);
+    dy = abs(dy);
+    float t = linewidth/2.0 - antialias;
+
+    // Round
+    if (type == CAP_ROUND)
+        d = sqrt(dx*dx+dy*dy);
+
+    // Square
+    else if (type == CAP_SQUARE)
+        d = max(dx,dy);
+
+    // Butt
+    else if (type == CAP_BUTT)
+        d = max(dx+t,dy);
+
+    // Triangle in
+    else if (type == CAP_TRIANGLE_IN)
+        d = (dx+abs(dy));
+
+    // Triangle out
+    else if (type == CAP_TRIANGLE_OUT)
+        d = max(abs(dy),(t+dx-abs(dy)));
+
+    // None
+    else
+        discard;
+
+    return stroke(d, linewidth, antialias, color);
+}
diff --git a/vispy/glsl/antialias/caps.glsl b/vispy/glsl/antialias/caps.glsl
new file mode 100644
index 0000000..b5241af
--- /dev/null
+++ b/vispy/glsl/antialias/caps.glsl
@@ -0,0 +1,67 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/stroke.glsl"
+
+// Cap types
+// ----------------------------
+const int CAP_NONE         = 0;
+const int CAP_ROUND        = 1;
+const int CAP_TRIANGLE_IN  = 2;
+const int CAP_TRIANGLE_OUT = 3;
+const int CAP_SQUARE       = 4;
+const int CAP_BUTT         = 5;
+
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for a line cap.
+   Require the stroke function.
+
+   Parameters:
+   -----------
+
+   type     : Type of cap
+   dx,dy    : signed distances to cap point (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   stroke:    Stroke color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+vec4 cap(int type, float dx, float dy, float linewidth, float antialias, vec4 color)
+{
+    float d = 0.0;
+    dx = abs(dx);
+    dy = abs(dy);
+    float t = linewidth/2.0 - antialias;
+
+    // Round
+    if (type == CAP_ROUND)
+        d = sqrt(dx*dx+dy*dy);
+
+    // Square
+    else if (type == CAP_SQUARE)
+        d = max(dx,dy);
+
+    // Butt
+    else if (type == CAP_BUTT)
+        d = max(dx+t,dy);
+
+    // Triangle in
+    else if (type == CAP_TRIANGLE_IN)
+        d = (dx+abs(dy));
+
+    // Triangle out
+    else if (type == CAP_TRIANGLE_OUT)
+        d = max(abs(dy),(t+dx-abs(dy)));
+
+    // None
+    else
+        discard;
+
+    return stroke(d, linewidth, antialias, color);
+}
diff --git a/vispy/glsl/antialias/filled.glsl b/vispy/glsl/antialias/filled.glsl
new file mode 100644
index 0000000..dc2ecb7
--- /dev/null
+++ b/vispy/glsl/antialias/filled.glsl
@@ -0,0 +1,45 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for a filled shape.
+
+   Parameters:
+   -----------
+
+   distance : signed distance to border (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   fill:      Fill color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+
+vec4 filled(float distance, float linewidth, float antialias, vec4 bg_color)
+{
+    vec4 frag_color;
+    float t = linewidth/2.0 - antialias;
+    float signed_distance = distance;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/antialias;
+    alpha = exp(-alpha*alpha);
+
+    if( border_distance < 0.0 )
+        frag_color = bg_color;
+    else if( signed_distance < 0.0 )
+        frag_color = bg_color;
+    else
+        frag_color = vec4(bg_color.rgb, alpha * bg_color.a);
+
+    return frag_color;
+}
+
+vec4 filled(float distance, float linewidth, float antialias, vec4 fg_color, vec4 bg_color)
+{
+    return filled(distance, linewidth, antialias, fg_color);
+}
diff --git a/vispy/glsl/antialias/outline.glsl b/vispy/glsl/antialias/outline.glsl
new file mode 100644
index 0000000..f1d8f7e
--- /dev/null
+++ b/vispy/glsl/antialias/outline.glsl
@@ -0,0 +1,40 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for an outlined shape.
+
+   Parameters:
+   -----------
+
+   distance : signed distance to border (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   stroke:    Stroke color
+   fill:      Fill color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+
+vec4 outline(float distance, float linewidth, float antialias, vec4 fg_color, vec4 bg_color)
+{
+    vec4 frag_color;
+    float t = linewidth/2.0 - antialias;
+    float signed_distance = distance;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/antialias;
+    alpha = exp(-alpha*alpha);
+
+    if( border_distance < 0.0 )
+        frag_color = fg_color;
+    else if( signed_distance < 0.0 )
+        frag_color = mix(bg_color, fg_color, sqrt(alpha));
+    else
+        frag_color = vec4(fg_color.rgb, fg_color.a * alpha);
+    return frag_color;
+}
diff --git a/vispy/glsl/antialias/stroke.glsl b/vispy/glsl/antialias/stroke.glsl
new file mode 100644
index 0000000..62d68e9
--- /dev/null
+++ b/vispy/glsl/antialias/stroke.glsl
@@ -0,0 +1,43 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+   Compute antialiased fragment color for a stroke line.
+
+   Parameters:
+   -----------
+
+   distance : signed distance to border (in pixels)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+   stroke:    Stroke color
+
+   Return:
+   -------
+   Fragment color (vec4)
+
+   --------------------------------------------------------- */
+
+vec4 stroke(float distance, float linewidth, float antialias, vec4 fg_color)
+{
+    vec4 frag_color;
+    float t = linewidth/2.0 - antialias;
+    float signed_distance = distance;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/antialias;
+    alpha = exp(-alpha*alpha);
+
+    if( border_distance < 0.0 )
+        frag_color = fg_color;
+    else
+        frag_color = vec4(fg_color.rgb, fg_color.a * alpha);
+
+    return frag_color;
+}
+
+vec4 stroke(float distance, float linewidth, float antialias, vec4 fg_color, vec4 bg_color)
+{
+    return stroke(distance, linewidth, antialias, fg_color);
+}
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/glsl/arrows/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/glsl/arrows/__init__.py
diff --git a/vispy/glsl/arrows/angle-30.glsl b/vispy/glsl/arrows/angle-30.glsl
new file mode 100644
index 0000000..e9894df
--- /dev/null
+++ b/vispy/glsl/arrows/angle-30.glsl
@@ -0,0 +1,12 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "arrows/util.glsl"
+
+float arrow_angle_30(vec2 texcoord,
+                     float body, float head,
+                     float linewidth, float antialias)
+{
+    return arrow_angle(texcoord, body, head, 0.25, linewidth, antialias);
+}
diff --git a/vispy/glsl/arrows/angle-60.glsl b/vispy/glsl/arrows/angle-60.glsl
new file mode 100644
index 0000000..62ca589
--- /dev/null
+++ b/vispy/glsl/arrows/angle-60.glsl
@@ -0,0 +1,12 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "arrows/util.glsl"
+
+float arrow_angle_60(vec2 texcoord,
+                     float body, float head,
+                     float linewidth, float antialias)
+{
+    return arrow_angle(texcoord, body, head, 0.5, linewidth, antialias);
+}
diff --git a/vispy/glsl/arrows/angle-90.glsl b/vispy/glsl/arrows/angle-90.glsl
new file mode 100644
index 0000000..d29febb
--- /dev/null
+++ b/vispy/glsl/arrows/angle-90.glsl
@@ -0,0 +1,12 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "arrows/util.glsl"
+
+float arrow_angle_90(vec2 texcoord,
+                     float body, float head,
+                     float linewidth, float antialias)
+{
+    return arrow_angle(texcoord, body, head, 1.0, linewidth, antialias);
+}
diff --git a/vispy/glsl/arrows/arrow.frag b/vispy/glsl/arrows/arrow.frag
new file mode 100644
index 0000000..57749ca
--- /dev/null
+++ b/vispy/glsl/arrows/arrow.frag
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <paint>  : "stroke", "filled" or "outline"
+//  <marker> : "steath", "curved",
+//             "angle_30", "angle_60", "angle_90",
+//             "triangle_30", "triangle_60", "triangle_90",
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+#include "arrows/arrows.glsl"
+#include "antialias/antialias.glsl"
+
+// Varyings
+// ------------------------------------
+varying float v_antialias;
+varying float v_linewidth;
+varying float v_size;
+varying float v_head;
+varying float v_texcoord;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying vec2  v_orientation;
+
+// Main (hooked)
+// ------------------------------------
+void main()
+{
+    vec2 P = gl_PointCoord.xy - vec2(0.5,0.5);
+    P = vec2(v_orientation.x*P.x - v_orientation.y*P.y,
+             v_orientation.y*P.x + v_orientation.x*P.y) * v_size;
+    float point_size = M_SQRT2*v_size  + 2.0 * (v_linewidth + 1.5*v_antialias);
+    float body = v_size/M_SQRT2;
+
+    float distance = arrow_<arrow>(P, body, v_head*body, v_linewidth, v_antialias);
+    gl_FragColor = <paint>(distance, v_linewidth, v_antialias, v_fg_color, v_bg_color);
+}
diff --git a/vispy/glsl/arrows/arrow.vert b/vispy/glsl/arrows/arrow.vert
new file mode 100644
index 0000000..73b7d4c
--- /dev/null
+++ b/vispy/glsl/arrows/arrow.vert
@@ -0,0 +1,49 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <transform> : vec4 function(position, ...)
+//
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+// Uniforms
+// ------------------------------------
+uniform float antialias;
+
+// Attributes
+// ------------------------------------
+attribute vec2  position;
+attribute float size;
+attribute float head;
+attribute vec4  fg_color;
+attribute vec4  bg_color;
+attribute float orientation;
+attribute float linewidth;
+
+// Varyings
+// ------------------------------------
+varying float v_size;
+varying float v_head;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying vec2  v_orientation;
+varying float v_antialias;
+varying float v_linewidth;
+
+// Main (hooked)
+// ------------------------------------
+void main (void)
+{
+    v_size        = size;
+    v_head        = head;
+    v_linewidth   = linewidth;
+    v_antialias   = antialias;
+    v_fg_color    = fg_color;
+    v_bg_color    = bg_color;
+    v_orientation = vec2(cos(orientation), sin(orientation));
+
+    gl_Position = <transform>;
+    gl_PointSize = M_SQRT2 * size + 2.0 * (linewidth + 1.5*antialias);
+}
diff --git a/vispy/glsl/arrows/arrows.glsl b/vispy/glsl/arrows/arrows.glsl
new file mode 100644
index 0000000..292e3eb
--- /dev/null
+++ b/vispy/glsl/arrows/arrows.glsl
@@ -0,0 +1,17 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+#include "arrows/util.glsl"
+
+#include "arrows/curved.glsl"
+#include "arrows/stealth.glsl"
+
+#include "arrows/angle-30.glsl"
+#include "arrows/angle-60.glsl"
+#include "arrows/angle-90.glsl"
+
+#include "arrows/triangle-30.glsl"
+#include "arrows/triangle-60.glsl"
+#include "arrows/triangle-90.glsl"
diff --git a/vispy/glsl/arrows/common.glsl b/vispy/glsl/arrows/common.glsl
new file mode 100644
index 0000000..4afa0c8
--- /dev/null
+++ b/vispy/glsl/arrows/common.glsl
@@ -0,0 +1,187 @@
+/* -------------------------------------------------------------------------
+ * Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+ * Distributed under the (new) BSD License.
+ * -------------------------------------------------------------------------
+ */
+
+// Computes the signed distance from a line
+float line_distance(vec2 p, vec2 p1, vec2 p2) {
+    vec2 center = (p1 + p2) * 0.5;
+    float len = length(p2 - p1);
+    vec2 dir = (p2 - p1) / len;
+    vec2 rel_p = p - center;
+    return dot(rel_p, vec2(dir.y, -dir.x));
+}
+
+// Computes the signed distance from a line segment
+float segment_distance(vec2 p, vec2 p1, vec2 p2) {
+    vec2 center = (p1 + p2) * 0.5;
+    float len = length(p2 - p1);
+    vec2 dir = (p2 - p1) / len;
+    vec2 rel_p = p - center;
+    float dist1 = abs(dot(rel_p, vec2(dir.y, -dir.x)));
+    float dist2 = abs(dot(rel_p, dir)) - 0.5*len;
+    return max(dist1, dist2);
+}
+
+// Computes the center with given radius passing through p1 & p2
+vec4 circle_from_2_points(vec2 p1, vec2 p2, float radius)
+{
+    float q = length(p2-p1);
+    vec2 m = (p1+p2)/2.0;
+    vec2 d = vec2( sqrt(radius*radius - (q*q/4.0)) * (p1.y-p2.y)/q,
+                   sqrt(radius*radius - (q*q/4.0)) * (p2.x-p1.x)/q);
+    return  vec4(m+d, m-d);
+}
+
+float arrow_curved(vec2 texcoord,
+                   float body, float head,
+                   float linewidth, float antialias)
+{
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+    float height = 0.5;
+
+    vec2 p1 = end - head*vec2(+1.0,+height);
+    vec2 p2 = end - head*vec2(+1.0,-height);
+    vec2 p3 = end;
+
+    // Head : 3 circles
+    vec2 c1  = circle_from_2_points(p1, p3, 1.25*body).zw;
+    float d1 = length(texcoord - c1) - 1.25*body;
+    vec2 c2  = circle_from_2_points(p2, p3, 1.25*body).xy;
+    float d2 = length(texcoord - c2) - 1.25*body;
+    vec2 c3  = circle_from_2_points(p1, p2, max(body-head, 1.0*body)).xy;
+    float d3 = length(texcoord - c3) - max(body-head, 1.0*body);
+
+    // Body : 1 segment
+    float d4 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+
+    // Outside (because of circles)
+    if( texcoord.y > +(2.0*head + antialias) )
+         return 1000.0;
+    if( texcoord.y < -(2.0*head + antialias) )
+         return 1000.0;
+    if( texcoord.x < -(body/2.0 + antialias) )
+         return 1000.0;
+    if( texcoord.x > c1.x ) //(body + antialias) )
+         return 1000.0;
+
+    return min( d4, -min(d3,min(d1,d2)));
+}
+
+float arrow_triangle(vec2 texcoord,
+                     float body, float head, float height,
+                     float linewidth, float antialias)
+{
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+
+    // Head : 3 lines
+    float d1 = line_distance(texcoord, end, end - head*vec2(+1.0,-height));
+    float d2 = line_distance(texcoord, end - head*vec2(+1.0,+height), end);
+    float d3 = texcoord.x - end.x + head;
+
+    // Body : 1 segment
+    float d4 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+
+    float d = min(max(max(d1, d2), -d3), d4);
+    return d;
+}
+
+float arrow_triangle_90(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_triangle(texcoord, body, head, 1.0, linewidth, antialias);
+}
+
+float arrow_triangle_60(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_triangle(texcoord, body, head, 0.5, linewidth, antialias);
+}
+
+float arrow_triangle_30(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_triangle(texcoord, body, head, 0.25, linewidth, antialias);
+}
+
+float arrow_angle(vec2 texcoord,
+                  float body, float head, float height,
+                  float linewidth, float antialias)
+{
+    float d;
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+
+    // Arrow tip (beyond segment end)
+    if( texcoord.x > body/2.0) {
+        // Head : 2 segments
+        float d1 = line_distance(texcoord, end, end - head*vec2(+1.0,-height));
+        float d2 = line_distance(texcoord, end - head*vec2(+1.0,+height), end);
+        // Body : 1 segment
+        float d3 = end.x - texcoord.x;
+        d = max(max(d1,d2), d3);
+    } else {
+        // Head : 2 segments
+        float d1 = segment_distance(texcoord, end - head*vec2(+1.0,-height), end);
+        float d2 = segment_distance(texcoord, end - head*vec2(+1.0,+height), end);
+        // Body : 1 segment
+        float d3 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+        d = min(min(d1,d2), d3);
+    }
+    return d;
+}
+
+float arrow_angle_90(vec2 texcoord,
+                     float body, float head,
+                     float linewidth, float antialias)
+{
+    return arrow_angle(texcoord, body, head, 1.0, linewidth, antialias);
+}
+
+float arrow_angle_60(vec2 texcoord,
+                     float body, float head,
+                     float linewidth, float antialias)
+{
+    return arrow_angle(texcoord, body, head, 0.5, linewidth, antialias);
+}
+
+float arrow_angle_30(vec2 texcoord,
+                     float body, float head,
+                     float linewidth, float antialias)
+{
+    return arrow_angle(texcoord, body, head, 0.25, linewidth, antialias);
+}
+
+
+float arrow_stealth(vec2 texcoord,
+                    float body, float head,
+                    float linewidth, float antialias)
+{
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+    float height = 0.5;
+
+    // Head : 4 lines
+    float d1 = line_distance(texcoord, end-head*vec2(+1.0,-height),
+                                       end);
+    float d2 = line_distance(texcoord, end-head*vec2(+1.0,-height),
+                                       end-vec2(3.0*head/4.0,0.0));
+    float d3 = line_distance(texcoord, end-head*vec2(+1.0,+height), end);
+    float d4 = line_distance(texcoord, end-head*vec2(+1.0,+0.5),
+                                       end-vec2(3.0*head/4.0,0.0));
+
+    // Body : 1 segment
+    float d5 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+
+    return min(d5, max( max(-d1, d3), - max(-d2,d4)));
+}
diff --git a/vispy/glsl/arrows/curved.glsl b/vispy/glsl/arrows/curved.glsl
new file mode 100644
index 0000000..0d02214
--- /dev/null
+++ b/vispy/glsl/arrows/curved.glsl
@@ -0,0 +1,63 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "arrows/util.glsl"
+
+
+/* ---------------------------------------------------------
+
+   Computes the signed distance to an curved arrow
+
+   Parameters:
+   -----------
+
+   texcoord : Point to compute distance to
+   body :     Total length of the arrow (pixels, body+head)
+   head :     Length of the head (pixels)
+   height :   Height of the head (pixel)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+
+   Return:
+   -------
+   Signed distance to the arrow
+
+   --------------------------------------------------------- */
+
+float arrow_curved(vec2 texcoord,
+                   float body, float head,
+                   float linewidth, float antialias)
+{
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+    float height = 0.5;
+
+    vec2 p1 = end - head*vec2(+1.0,+height);
+    vec2 p2 = end - head*vec2(+1.0,-height);
+    vec2 p3 = end;
+
+    // Head : 3 circles
+    vec2 c1  = circle_from_2_points(p1, p3, 1.25*body).zw;
+    float d1 = length(texcoord - c1) - 1.25*body;
+    vec2 c2  = circle_from_2_points(p2, p3, 1.25*body).xy;
+    float d2 = length(texcoord - c2) - 1.25*body;
+    vec2 c3  = circle_from_2_points(p1, p2, max(body-head, 1.0*body)).xy;
+    float d3 = length(texcoord - c3) - max(body-head, 1.0*body);
+
+    // Body : 1 segment
+    float d4 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+
+    // Outside (because of circles)
+    if( texcoord.y > +(2.0*head + antialias) )
+         return 1000.0;
+    if( texcoord.y < -(2.0*head + antialias) )
+         return 1000.0;
+    if( texcoord.x < -(body/2.0 + antialias) )
+         return 1000.0;
+    if( texcoord.x > c1.x ) //(body + antialias) )
+         return 1000.0;
+
+    return min( d4, -min(d3,min(d1,d2)));
+}
diff --git a/vispy/glsl/arrows/stealth.glsl b/vispy/glsl/arrows/stealth.glsl
new file mode 100644
index 0000000..48fe855
--- /dev/null
+++ b/vispy/glsl/arrows/stealth.glsl
@@ -0,0 +1,50 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "arrows/util.glsl"
+
+
+/* ---------------------------------------------------------
+
+   Computes the signed distance to an stealth arrow
+
+   Parameters:
+   -----------
+
+   texcoord : Point to compute distance to
+   body :     Total length of the arrow (pixels, body+head)
+   head :     Length of the head (pixels)
+   height :   Height of the head (pixel)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+
+   Return:
+   -------
+   Signed distance to the arrow
+
+   --------------------------------------------------------- */
+
+float arrow_stealth(vec2 texcoord,
+                    float body, float head,
+                    float linewidth, float antialias)
+{
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+    float height = 0.5;
+
+    // Head : 4 lines
+    float d1 = line_distance(texcoord, end-head*vec2(+1.0,-height),
+                                       end);
+    float d2 = line_distance(texcoord, end-head*vec2(+1.0,-height),
+                                       end-vec2(3.0*head/4.0,0.0));
+    float d3 = line_distance(texcoord, end-head*vec2(+1.0,+height), end);
+    float d4 = line_distance(texcoord, end-head*vec2(+1.0,+0.5),
+                                       end-vec2(3.0*head/4.0,0.0));
+
+    // Body : 1 segment
+    float d5 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+
+    return min(d5, max( max(-d1, d3), - max(-d2,d4)));
+}
diff --git a/vispy/glsl/arrows/triangle-30.glsl b/vispy/glsl/arrows/triangle-30.glsl
new file mode 100644
index 0000000..ba72d36
--- /dev/null
+++ b/vispy/glsl/arrows/triangle-30.glsl
@@ -0,0 +1,12 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "arrows/util.glsl"
+
+float arrow_triangle_30(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_triangle(texcoord, body, head, 0.25, linewidth, antialias);
+}
diff --git a/vispy/glsl/arrows/triangle-60.glsl b/vispy/glsl/arrows/triangle-60.glsl
new file mode 100644
index 0000000..5eda6e3
--- /dev/null
+++ b/vispy/glsl/arrows/triangle-60.glsl
@@ -0,0 +1,12 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "arrows/util.glsl"
+
+float arrow_triangle_60(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_triangle(texcoord, body, head, 0.5, linewidth, antialias);
+}
diff --git a/vispy/glsl/arrows/triangle-90.glsl b/vispy/glsl/arrows/triangle-90.glsl
new file mode 100644
index 0000000..33bcf7b
--- /dev/null
+++ b/vispy/glsl/arrows/triangle-90.glsl
@@ -0,0 +1,12 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "arrows/util.glsl"
+
+float arrow_triangle_90(vec2 texcoord,
+                        float body, float head,
+                        float linewidth, float antialias)
+{
+    return arrow_triangle(texcoord, body, head, 1.0, linewidth, antialias);
+}
diff --git a/vispy/glsl/arrows/util.glsl b/vispy/glsl/arrows/util.glsl
new file mode 100644
index 0000000..2cad652
--- /dev/null
+++ b/vispy/glsl/arrows/util.glsl
@@ -0,0 +1,98 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "math/signed-line-distance.glsl"
+#include "math/point-to-line-distance.glsl"
+#include "math/signed-segment-distance.glsl"
+#include "math/circle-through-2-points.glsl"
+#include "math/point-to-line-projection.glsl"
+
+
+/* ---------------------------------------------------------
+
+   Computes the signed distance to a triangle arrow
+
+   Parameters:
+   -----------
+
+   texcoord : Point to compute distance to
+   body :     Total length of the arrow (pixels, body+head)
+   head :     Length of the head (pixels)
+   height :   Height of the head (pixel)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+
+   Return:
+   -------
+   Signed distance to the arrow
+
+   --------------------------------------------------------- */
+
+float arrow_triangle(vec2 texcoord,
+                     float body, float head, float height,
+                     float linewidth, float antialias)
+{
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+
+    // Head : 3 lines
+    float d1 = line_distance(texcoord, end, end - head*vec2(+1.0,-height));
+    float d2 = line_distance(texcoord, end - head*vec2(+1.0,+height), end);
+    float d3 = texcoord.x - end.x + head;
+
+    // Body : 1 segment
+    float d4 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+
+    float d = min(max(max(d1, d2), -d3), d4);
+    return d;
+}
+
+
+/* ---------------------------------------------------------
+
+   Computes the signed distance to an angle arrow
+
+   Parameters:
+   -----------
+
+   texcoord : Point to compute distance to
+   body :     Total length of the arrow (pixels, body+head)
+   head :     Length of the head (pixels)
+   height :   Height of the head (pixel)
+   linewidth: Stroke line width (in pixels)
+   antialias: Stroke antialiased area (in pixels)
+
+   Return:
+   -------
+   Signed distance to the arrow
+
+   --------------------------------------------------------- */
+float arrow_angle(vec2 texcoord,
+                  float body, float head, float height,
+                  float linewidth, float antialias)
+{
+    float d;
+    float w = linewidth/2.0 + antialias;
+    vec2 start = -vec2(body/2.0, 0.0);
+    vec2 end   = +vec2(body/2.0, 0.0);
+
+    // Arrow tip (beyond segment end)
+    if( texcoord.x > body/2.0) {
+        // Head : 2 segments
+        float d1 = line_distance(texcoord, end, end - head*vec2(+1.0,-height));
+        float d2 = line_distance(texcoord, end - head*vec2(+1.0,+height), end);
+        // Body : 1 segment
+        float d3 = end.x - texcoord.x;
+        d = max(max(d1,d2), d3);
+    } else {
+        // Head : 2 segments
+        float d1 = segment_distance(texcoord, end - head*vec2(+1.0,-height), end);
+        float d2 = segment_distance(texcoord, end - head*vec2(+1.0,+height), end);
+        // Body : 1 segment
+        float d3 = segment_distance(texcoord, start, end - vec2(linewidth,0.0));
+        d = min(min(d1,d2), d3);
+    }
+    return d;
+}
diff --git a/vispy/glsl/build-spatial-filters.py b/vispy/glsl/build-spatial-filters.py
new file mode 100644
index 0000000..22e7efb
--- /dev/null
+++ b/vispy/glsl/build-spatial-filters.py
@@ -0,0 +1,675 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# glumpy is an OpenGL framework for the fast visualization of numpy arrays.
+# Copyright (C) 2009-2011  Nicolas P. Rougier. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY NICOLAS P. ROUGIER ''AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL NICOLAS P. ROUGIER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and documentation are
+# those of the authors and should not be interpreted as representing official
+# policies, either expressed or implied, of Nicolas P. Rougier.
+# -----------------------------------------------------------------------------
+'''
+A filter is a shader that transform the current displayed texture. Since
+shaders cannot be easily serialized within the GPU, they have to be well
+structured on the python side such that we can possibly merge them into a
+single source code for both vertex and fragment. Consequently, there is a
+default code for both vertex and fragment with specific entry points such that
+filter knows where to insert their specific code (declarations, functions and
+call (or code) to be inserted in the main function).
+
+Spatial interpolation filter classes for OpenGL textures.
+
+Each filter generates a one-dimensional lookup table (weights value from 0 to
+ceil(radius)) that is uploaded to video memory (as a 1d texture) and is then
+read by the shader when necessary. It avoids computing weight values for each
+pixel. Furthemore, each 2D-convolution filter is separable and can be computed
+using 2 1D-convolution with same 1d-kernel (= the lookup table values).
+
+Available filters:
+
+  - Nearest  (radius 0.5)
+  - Bilinear (radius 1.0)
+  - Hanning (radius 1.0)
+  - Hamming (radius 1.0)
+  - Hermite (radius 1.0)
+  - Kaiser (radius 1.0)
+  - Quadric (radius 1.5)
+  - Bicubic (radius 2.0)
+  - CatRom (radius 2.0)
+  - Mitchell (radius 2.0)
+  - Spline16 (radius 2.0)
+  - Spline36 (radius 4.0)
+  - Gaussian (radius 2.0)
+  - Bessel (radius 3.2383)
+  - Sinc (radius 4.0)
+  - Lanczos (radius 4.0)
+  - Blackman (radius 4.0)
+
+
+Note::
+
+  Weights code has been translated from the antigrain geometry library
+  available at http://www.antigrain.com/
+'''
+
+
+import math
+import numpy as np
+
+
+class SpatialFilter(object):
+    ''' '''
+
+    def __init__(self, radius=1.0):
+        self.radius = radius
+
+    def weight(self, x):
+        '''
+        Return filter weight for a distance x.
+
+        :Parameters:
+            ``x`` : 0 < float < ceil(self.radius)
+                Distance to be used to compute weight.
+        '''
+        raise NotImplemented
+
+    def kernel(self, size=4*512):
+        radius = self.radius
+        r = int(max(1.0, math.ceil(radius)))
+        samples = size / r
+        n = size  # r*samples
+        kernel = np.zeros(n)
+        X = np.linspace(0, r, n)
+        for i in range(n):
+            kernel[i] = self.weight(X[i])
+        N = np.zeros(samples)
+        for i in range(r):
+            N += kernel[::+1][i*samples:(i+1)*samples]
+            N += kernel[::-1][i*samples:(i+1)*samples]
+        for i in range(r):
+            kernel[i*samples:(i+1)*samples:+1] /= N
+        return kernel
+
+    def filter_code(self):
+
+        n = int(math.ceil(self.radius))
+        filter_1 = 'filter1D_radius%d' % n
+        filter_2 = 'filter2D_radius%d' % n
+
+        code = ''
+        code += 'vec4\n'
+        code += '%s( sampler2D kernel, float index, float x, ' % filter_1
+        for i in range(2*n):
+            if i == 2*n-1:
+                code += 'vec4 c%d )\n' % i
+            else:
+                code += 'vec4 c%d, ' % i
+        code += '{\n'
+        code += '    float w, w_sum = 0.0;\n'
+        code += '    vec4 r = vec4(0.0,0.0,0.0,0.0);\n'
+        for i in range(n):
+            code += '    w = texture2D(kernel, vec2(%f+(x/%.1f),index) ).r;\n' % (1.0 - (i + 1) / float(n), n)  # noqa
+            code += '    w = w*kernel_scale + kernel_bias;\n'  # noqa
+            # code += '   w_sum += w;'
+            code += '    r += c%d * w;\n' % i
+            code += '    w = texture2D(kernel, vec2(%f-(x/%.1f),index) ).r;\n' % ((i+1)/float(n), n)  # noqa
+            code += '    w = w*kernel_scale + kernel_bias;\n'
+            # code += '   w_sum += w;'
+            code += '    r += c%d * w;\n' % (i + n)
+        # code += '    return r/w_sum;\n'
+        code += '    return r;\n'
+        code += '}\n'
+        code += 'vec4\n'
+        code += '%s' % filter_2
+        code += '(sampler2D texture, sampler2D kernel, float index, vec2 uv, vec2 pixel )\n'  # noqa
+        code += '{\n'
+        code += '    vec2 texel = uv/pixel - vec2(0.0,0.0) ;\n'
+        code += '    vec2 f = fract(texel);\n'
+        code += '    texel = (texel-fract(texel)+vec2(0.001,0.001))*pixel;\n'
+        for i in range(2*n):
+            code += '    vec4 t%d = %s(kernel, index, f.x,\n' % (i, filter_1)
+            for j in range(2*n):
+                x, y = (-n+1+j, -n+1+i)
+                code += '        texture2D( texture, texel + vec2(%d,%d)*pixel),\n' % (x, y)  # noqa
+
+            # Remove last trailing',' and close function call
+            code = code[:-2] + ');\n'
+
+        code += '    return %s(kernel, index, f.y, ' % filter_1
+        for i in range(2*n):
+            code += 't%d, ' % i
+
+        # Remove last trailing',' and close function call
+        code = code[:-2] + ');\n'
+        code += '}\n'
+
+        return code
+
+    def call_code(self, index):
+        code = ""
+        n = int(math.ceil(self.radius))
+        filter_1 = 'filter1D_radius%d' % n  # noqa
+        filter_2 = 'filter2D_radius%d' % n
+
+        code += 'vec4 %s(sampler2D texture, vec2 shape, vec2 uv)\n' % self.__class__.__name__  # noqa
+        code += '{'
+        code += ' return %s(texture, u_kernel, %f, uv, 1.0/shape); ' % (filter_2, index)  # noqa
+        code += '}\n'
+        return code
+
+
+class Nearest(SpatialFilter):
+    '''
+    Nearest (=None) filter (radius = 0.5).
+
+    Weight function::
+
+      w(x) = 1
+
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=.5)
+
+    def weight(self, x):
+        return 1.0
+
+    def _get_code(self):
+        self.build_LUT()
+        code = 'vec4\n'
+        code += 'interpolate( sampler2D texture, sampler1D kernel, vec2 uv, vec2 pixel )\n'  # noqa
+        code += '{\n   return texture2D( texture, uv );\n}\n'
+        return code
+    code = property(_get_code, doc='''filter functions code''')
+
+
+class Bilinear(SpatialFilter):
+    '''
+    Bilinear filter (radius = 1.0).
+
+    Weight function::
+
+      w(x) = 1 - x
+
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=1.0)
+
+    def weight(self, x):
+        return 1.0 - x
+
+
+class Hanning(SpatialFilter):
+    '''
+    Hanning filter (radius = 1.0).
+
+    Weight function::
+
+      w(x) = 0.5 + 0.5 * cos(pi * x)
+
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=1.0)
+
+    def weight(self, x):
+        return 0.5 + 0.5 * math.cos(math.pi * x)
+
+
+class Hamming(SpatialFilter):
+    '''
+    Hamming filter (radius = 1.0).
+
+    Weight function::
+
+      w(x) = 0.54 + 0.46 * cos(pi * x)
+
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=1.0)
+
+    def weight(self, x):
+        return 0.54 + 0.46 * math.cos(math.pi * x)
+
+
+class Hermite(SpatialFilter):
+    ''' Hermite filter (radius = 1.0).
+
+    Weight function::
+
+      w(x) = (2*x-3)*x^2 + 1
+
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=1.0)
+
+    def weight(self, x):
+        return (2.0 * x - 3.0) * x * x + 1.0
+
+
+class Quadric(SpatialFilter):
+    '''
+    Quadric filter (radius = 1.5).
+
+    Weight function::
+
+             |  0.0 ≤ x < 0.5: 0.75 - x*x
+      w(x) = |  0.5 ≤ x < 1.5: 0.5 - (x-1.5)^2
+             |  1.5 ≤ x      : 0
+
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=1.5)
+
+    def weight(self, x):
+        if x < 0.75:
+            return 0.75 - x * x
+        elif x < 1.5:
+            t = x - 1.5
+            return 0.5 * t * t
+        else:
+            return 0.0
+
+
+class Bicubic(SpatialFilter):
+    '''
+    Bicubic filter (radius = 2.0).
+
+    Weight function::
+
+      w(x) = 1/6((x+2)^3 - 4*(x+1)^3 + 6*x^3 -4*(x-1)^3)
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=2.0)
+
+    def pow3(self, x):
+        if x <= 0:
+            return 0
+        else:
+            return x * x * x
+
+    def weight(self, x):
+        return (1.0/6.0) * (self.pow3(x + 2) -
+                            4 * self.pow3(x + 1) +
+                            6 * self.pow3(x) -
+                            4 * self.pow3(x - 1))
+
+
+class Kaiser(SpatialFilter):
+    '''
+    Kaiser filter (radius = 1.0).
+
+
+    Weight function::
+
+      w(x) = bessel_i0(a sqrt(1-x^2)* 1/bessel_i0(b)
+
+    '''
+
+    def __init__(self, b=6.33):
+        self.a = b
+        self.epsilon = 1e-12
+        self.i0a = 1.0 / self.bessel_i0(b)
+        SpatialFilter.__init__(self, radius=1.0)
+
+    def bessel_i0(self, x):
+        s = 1.0
+        y = x * x / 4.0
+        t = y
+        i = 2
+        while t > self.epsilon:
+            s += t
+            t *= float(y) / (i * i)
+            i += 1
+        return s
+
+    def weight(self, x):
+        if x > 1:
+            return 0
+        return self.bessel_i0(self.a * math.sqrt(1.0 - x * x)) * self.i0a
+
+
+class CatRom(SpatialFilter):
+    '''
+    Catmull-Rom filter (radius = 2.0).
+
+    Weight function::
+
+             |  0 ≤ x < 1: 0.5*(2 + x^2*(-5+x*3))
+      w(x) = |  1 ≤ x < 2: 0.5*(4 + x*(-8+x*(5-x)))
+             |  2 ≤ x    : 0
+
+    '''
+
+    def __init__(self, size=256*8):
+        SpatialFilter.__init__(self, radius=2.0)
+
+    def weight(self, x):
+        if x < 1.0:
+            return 0.5 * (2.0 + x * x * (-5.0 + x * 3.0))
+        elif x < 2.0:
+            return 0.5 * (4.0 + x * (-8.0 + x * (5.0 - x)))
+        else:
+            return 0.0
+
+
+class Mitchell(SpatialFilter):
+    '''
+    Mitchell-Netravali filter (radius = 2.0).
+
+    Weight function::
+
+             |  0 ≤ x < 1: p0 + x^2*(p2 + x*p3)
+      w(x) = |  1 ≤ x < 2: q0 + x*(q1 + x*(q2 + x*q3))
+             |  2 ≤ x    : 0
+
+    '''
+
+    def __init__(self, b=1.0/3.0, c=1.0/3.0):
+        self.p0 = (6.0 - 2.0 * b) / 6.0
+        self.p2 = (-18.0 + 12.0 * b + 6.0 * c) / 6.0
+        self.p3 = (12.0 - 9.0 * b - 6.0 * c) / 6.0
+        self.q0 = (8.0 * b + 24.0 * c) / 6.0
+        self.q1 = (-12.0 * b - 48.0 * c) / 6.0
+        self.q2 = (6.0 * b + 30.0 * c) / 6.0
+        self.q3 = (-b - 6.0 * c) / 6.0
+        SpatialFilter.__init__(self, radius=2.0)
+
+    def weight(self, x):
+        if x < 1.0:
+            return self.p0 + x * x * (self.p2 + x * self.p3)
+        elif x < 2.0:
+            return self.q0 + x * (self.q1 + x * (self.q2 + x * self.q3))
+        else:
+            return 0.0
+
+
+class Spline16(SpatialFilter):
+    '''
+    Spline16 filter (radius = 2.0).
+
+    Weight function::
+
+             |  0 ≤ x < 1: ((x-9/5)*x - 1/5)*x + 1
+      w(x) = |
+             |  1 ≤ x < 2: ((-1/3*(x-1) + 4/5)*(x-1) - 7/15 )*(x-1)
+
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=2.0)
+
+    def weight(self, x):
+        if x < 1.0:
+            return ((x - 9.0/5.0) * x - 1.0/5.0) * x + 1.0
+        else:
+            return ((-1.0/3.0 * (x-1) + 4.0/5.0) * (x-1) - 7.0/15.0) * (x-1)
+
+
+class Spline36(SpatialFilter):
+    '''
+    Spline36 filter (radius = 3.0).
+
+    Weight function::
+
+             |  0 ≤ x < 1: ((13/11*x - 453/209)*x -3/209)*x +1
+      w(x) = |  1 ≤ x < 2: ((-6/11*(x-1) - 270/209)*(x-1) -156/209)*(x-1)
+             |  2 ≤ x < 3: (( 1/11*(x-2) - 45/209)*(x-2) + 26/209)*(x-2)
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=3.0)
+
+    def weight(self, x):
+        if x < 1.0:
+            return ((13.0/11.0 * x - 453.0/209.0) * x - 3.0/209.0) * x + 1.0
+        elif x < 2.0:
+            return ((-6.0/11.0 * (x-1) + 270.0/209.0) * (x-1) - 156.0 / 209.0) * (x-1)  # noqa
+        else:
+            return ((1.0 / 11.0 * (x-2) - 45.0/209.0) * (x - 2) + 26.0/209.0) * (x-2)  # noqa
+
+
+class Gaussian(SpatialFilter):
+    '''
+    Gaussian filter (radius = 2.0).
+
+    Weight function::
+
+      w(x) = exp(-2x^2) * sqrt(2/pi)
+
+    Note::
+
+      This filter does not seem to be correct since:
+
+        x = np.linspace(0, 1.0, 100 )
+        f = weight
+        z = f(x+1)+f(x)+f(1-x)+f(2-x)
+
+        z should be 1 everywhere but it is not the case and it produces "grid
+        effects".
+    '''
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=2.0)
+
+    def weight(self, x):
+        return math.exp(-2.0 * x * x) * math.sqrt(2.0 / math.pi)
+
+
+class Bessel(SpatialFilter):
+    '''
+    Bessel filter (radius = 3.2383).
+    '''
+
+    def __init__(self):
+        SpatialFilter.__init__(self, radius=3.2383)
+
+    def besj(self, x, n):
+        '''
+        Function BESJ calculates Bessel function of first kind of order n
+        Arguments:
+            n - an integer (>=0), the order
+            x - value at which the Bessel function is required
+        --------------------
+        C++ Mathematical Library
+        Converted from equivalent FORTRAN library
+        Converted by Gareth Walker for use by course 392 computational project
+        All functions tested and yield the same results as the corresponding
+        FORTRAN versions.
+
+        If you have any problems using these functions please report them to
+        M.Muldoon at UMIST.ac.uk
+
+        Documentation available on the web
+        http://www.ma.umist.ac.uk/mrm/Teaching/392/libs/392.html
+        Version 1.0   8/98
+        29 October, 1999
+        --------------------
+        Adapted for use in AGG library by
+                    Andy Wilk (castor.vulgaris at gmail.com)
+        Adapted for use in vispy library by
+                    Nicolas P. Rougier (Nicolas.Rougier at inria.fr)
+        -----------------------------------------------------------------------
+        '''
+        if n < 0:
+            return 0.0
+
+        d = 1e-6
+        b = 0
+        if math.fabs(x) <= d:
+            if n != 0:
+                return 0
+            return 1
+
+        b1 = 0  # b1 is the value from the previous iteration
+        # Set up a starting order for recurrence
+        m1 = int(math.fabs(x)) + 6
+        if math.fabs(x) > 5:
+            m1 = int(math.fabs(1.4 * x + 60 / x))
+
+        m2 = int(n + 2 + math.fabs(x) / 4)
+        if m1 > m2:
+            m2 = m1
+
+        # Apply recurrence down from curent max order
+        while True:
+            c3 = 0
+            c2 = 1e-30
+            c4 = 0
+            m8 = 1
+            if m2 / 2 * 2 == m2:
+                m8 = -1
+
+            imax = m2 - 2
+            for i in range(1, imax+1):
+                c6 = 2 * (m2 - i) * c2 / x - c3
+                c3 = c2
+                c2 = c6
+                if m2 - i - 1 == n:
+                    b = c6
+                m8 = -1 * m8
+                if m8 > 0:
+                    c4 = c4 + 2 * c6
+
+            c6 = 2 * c2 / x - c3
+            if n == 0:
+                b = c6
+            c4 += c6
+            b /= c4
+            if math.fabs(b - b1) < d:
+                return b
+            b1 = b
+            m2 += 3
+
+    def weight(self, x):
+        if x == 0.0:
+            return math.pi/4.0
+        else:
+            return self.besj(math.pi * x, 1) / (2.0 * x)
+
+
+class Sinc(SpatialFilter):
+    '''
+    Sinc filter (radius = 4.0).
+
+    Weight function::
+
+
+    '''
+
+    def __init__(self, size=256, radius=4.0):
+        SpatialFilter.__init__(self, radius=max(radius, 2.0))
+
+    def weight(self, x):
+        if x == 0.0:
+            return 1.0
+        x *= math.pi
+        return (math.sin(x) / x)
+
+
+class Lanczos(SpatialFilter):
+    '''
+    Lanczos filter (radius = 4.0).
+
+    Weight function::
+
+
+    '''
+
+    def __init__(self, size=256, radius=4.0):
+        SpatialFilter.__init__(self, radius=max(radius, 2.0))
+
+    def weight(self, x):
+        if x == 0.0:
+            return 1.0
+        elif x > self.radius:
+            return 0.0
+        x *= math.pi
+        xr = x / self.radius
+        return (math.sin(x) / x) * (math.sin(xr)/xr)
+
+
+class Blackman(SpatialFilter):
+    '''
+    Blackman filter (radius = 4.0).
+    '''
+
+    def __init__(self, size=256, radius=4.0):
+        SpatialFilter.__init__(self, radius=max(radius, 2.0))
+
+    def weight(self, x):
+        if x == 0.0:
+            return 1.0
+        elif x > self.radius:
+            return 0.0
+        x *= math.pi
+        xr = x / self.radius
+        return (math.sin(x) / x) * (0.42 + 0.5*math.cos(xr) + 0.08*math.cos(2*xr))  # noqa
+
+
+# Generate kernels texture (16 x 1024)
+filters = [Bilinear(), Hanning(),  Hamming(),  Hermite(),
+           Kaiser(),   Quadric(),  Bicubic(),  CatRom(),
+           Mitchell(), Spline16(), Spline36(), Gaussian(),
+           Bessel(),   Sinc(),     Lanczos(),  Blackman()]
+
+n = 1024
+K = np.zeros((16, n))
+for i, f in enumerate(filters):
+    K[i] = f.kernel(n)
+
+bias = K.min()
+scale = K.max()-K.min()
+K = (K-bias)/scale
+np.save("spatial-filters.npy", K.astype(np.float32))
+
+print("// ------------------------------------")
+print("// Automatically generated, do not edit")
+print("// ------------------------------------")
+print("")
+print("const float kernel_bias  = %f;" % bias)
+print("const float kernel_scale = %f;" % scale)
+print("uniform sampler2D u_kernel;")
+print("")
+
+F = SpatialFilter(1.0)
+print(F.filter_code())
+F = SpatialFilter(2.0)
+print(F.filter_code())
+F = SpatialFilter(3.0)
+print(F.filter_code())
+F = SpatialFilter(4.0)
+print(F.filter_code())
+
+# Generate filter functions
+# Special case for nearest
+print("""vec4 Nearest(sampler2D texture, vec2 shape, vec2 uv)""")
+print("""{ return texture2D(texture,uv); }\n""")
+
+for i, f in enumerate(filters):
+    print(f.call_code((i+0.5)/16.0))
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/glsl/collections/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/glsl/collections/__init__.py
diff --git a/vispy/glsl/collections/agg-fast-path.frag b/vispy/glsl/collections/agg-fast-path.frag
new file mode 100644
index 0000000..3ae15b6
--- /dev/null
+++ b/vispy/glsl/collections/agg-fast-path.frag
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/antialias.glsl"
+
+// Varyings
+// ------------------------------------
+varying vec4  v_color;
+varying float v_distance;
+varying float v_linewidth;
+varying float v_antialias;
+
+// Main
+// ------------------------------------
+void main()
+{
+    if (v_color.a == 0.)  { discard; }
+    gl_FragColor = stroke(v_distance, v_linewidth, v_antialias, v_color);
+}
diff --git a/vispy/glsl/collections/agg-fast-path.vert b/vispy/glsl/collections/agg-fast-path.vert
new file mode 100644
index 0000000..cde9db0
--- /dev/null
+++ b/vispy/glsl/collections/agg-fast-path.vert
@@ -0,0 +1,78 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <transform> : vec4 function(position, ...)
+//
+// ----------------------------------------------------------------------------
+#include "misc/viewport-NDC.glsl"
+
+// Externs
+// ------------------------------------
+// extern vec3  prev;
+// extern vec3  curr;
+// extern vec3  next;
+// extern float id;
+// extern vec4  color;
+// extern float antialias;
+// extern float linewidth;
+// extern vec4 viewport;
+//        vec4 transform(vec3 position);
+
+// Varyings
+// ------------------------------------
+varying float v_antialias;
+varying float v_linewidth;
+varying float v_distance;
+varying vec4  v_color;
+
+
+// Main
+// ------------------------------------
+void main (void)
+{
+    // This function is externally generated
+    fetch_uniforms();
+    v_linewidth = linewidth;
+    v_antialias = antialias;
+    v_color     = color;
+
+    // transform prev/curr/next
+    vec4 prev_ = $transform(vec4(prev, 1));
+    vec4 curr_ = $transform(vec4(curr, 1));
+    vec4 next_ = $transform(vec4(next, 1));
+
+    // prev/curr/next in viewport coordinates
+    vec2 _prev = NDC_to_viewport(prev_, viewport.zw);
+    vec2 _curr = NDC_to_viewport(curr_, viewport.zw);
+    vec2 _next = NDC_to_viewport(next_, viewport.zw);
+
+    // Compute vertex final position (in viewport coordinates)
+    float w = linewidth/2.0 + 1.5*antialias;
+    float z;
+    vec2 P;
+    if( curr == prev) {
+        vec2 v = normalize(_next.xy - _curr.xy);
+        vec2 normal = normalize(vec2(-v.y,v.x));
+        P = _curr.xy + normal*w*id;
+    } else if (curr == next) {
+        vec2 v = normalize(_curr.xy - _prev.xy);
+        vec2 normal  = normalize(vec2(-v.y,v.x));
+        P = _curr.xy + normal*w*id;
+    } else {
+        vec2 v0 = normalize(_curr.xy - _prev.xy);
+        vec2 v1 = normalize(_next.xy - _curr.xy);
+        vec2 normal  = normalize(vec2(-v0.y,v0.x));
+        vec2 tangent = normalize(v0+v1);
+        vec2 miter   = vec2(-tangent.y, tangent.x);
+        float l = abs(w / dot(miter,normal));
+        P = _curr.xy + miter*l*sign(id);
+    }
+
+    if( abs(id) > 1.5 ) v_color.a = 0.0;
+
+    v_distance = w*id;
+    gl_Position = viewport_to_NDC(vec3(P, curr_.z/curr_.w), viewport.zw);
+
+}
diff --git a/vispy/glsl/collections/agg-glyph.frag b/vispy/glsl/collections/agg-glyph.frag
new file mode 100644
index 0000000..2eb4f8d
--- /dev/null
+++ b/vispy/glsl/collections/agg-glyph.frag
@@ -0,0 +1,60 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Uniforms
+// ------------------------------------
+uniform sampler2D atlas_data;
+uniform vec2      atlas_shape;
+
+// Varyings
+// ------------------------------------
+varying vec4  v_color;
+varying float v_offset;
+varying vec2  v_texcoord;
+
+
+// Main
+// ------------------------------------
+void main(void)
+{
+    <viewport.clipping>;
+
+    vec2 viewport = <viewport.viewport_global>.zw;
+
+    vec4 current = texture2D(atlas_data, v_texcoord);
+    vec4 previous= texture2D(atlas_data, v_texcoord+vec2(-1.0,0.0)/viewport);
+    vec4 next    = texture2D(atlas_data, v_texcoord+vec2(+1.0,0.0)/viewport);
+
+    float r = current.r;
+    float g = current.g;
+    float b = current.b;
+
+    if( v_offset < 1.0 )
+    {
+        float z = v_offset;
+        r = mix(current.r, previous.b, z);
+        g = mix(current.g, current.r,  z);
+        b = mix(current.b, current.g,  z);
+    }
+    else if( v_offset < 2.0 )
+    {
+        float z = v_offset - 1.0;
+        r = mix(previous.b, previous.g, z);
+        g = mix(current.r,  previous.b, z);
+        b = mix(current.g,  current.r,  z);
+    }
+   else //if( v_offset <= 1.0 )
+    {
+        float z = v_offset - 2.0;
+        r = mix(previous.g, previous.r, z);
+        g = mix(previous.b, previous.g, z);
+        b = mix(current.r,  previous.b, z);
+    }
+
+   float t = max(max(r,g),b);
+   vec4 color = vec4(v_color.rgb, (r+g+b)/3.0);
+   color = t*color + (1.0-t)*vec4(r,g,b, min(min(r,g),b));
+   gl_FragColor = vec4( color.rgb, v_color.a*color.a);
+}
diff --git a/vispy/glsl/collections/agg-glyph.vert b/vispy/glsl/collections/agg-glyph.vert
new file mode 100644
index 0000000..b37d113
--- /dev/null
+++ b/vispy/glsl/collections/agg-glyph.vert
@@ -0,0 +1,33 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Externs
+// ------------------------------------
+// vec2 origin;
+// vec2 position;
+// vec2 texcoord;
+// vec4 color;
+
+// Varyings
+// ------------------------------------
+varying vec4  v_color;
+varying float v_offset;
+varying vec2  v_texcoord;
+
+// Main
+// ------------------------------------
+void main()
+{
+    fetch_uniforms();
+
+    gl_Position = <transform(origin)>;
+    v_color = color;
+    v_texcoord = texcoord;
+    <viewport.transform>;
+
+    // We set actual position after transform
+    v_offset = 3.0*(offset + origin.x - int(origin.x));
+    gl_Position = gl_Position + vec4(2.0*position/<viewport.viewport_global>.zw,0,0);
+}
diff --git a/vispy/glsl/collections/agg-marker.frag b/vispy/glsl/collections/agg-marker.frag
new file mode 100644
index 0000000..bfe5d3d
--- /dev/null
+++ b/vispy/glsl/collections/agg-marker.frag
@@ -0,0 +1,35 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <paint>  : "stroke", "filled" or "outline"
+//  <marker> : "arrow", "asterisk", "chevron", "clover", "club",
+//             "cross", "diamond", "disc", "ellipse", "hbar",
+//             "heart", "infinity", "pin", "ring", "spade",
+//             "square", "tag", "triangle", "vbar"
+// ----------------------------------------------------------------------------
+#include "math/constants.h"
+#include "markers/markers.glsl"
+#include "antialias/antialias.h"
+
+// Varyings
+// ------------------------------------
+varying float v_antialias;
+varying float v_linewidth;
+varying float v_size;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying vec2  v_orientation;
+
+// Main (hooked)
+// ------------------------------------
+void main()
+{
+    vec2 P = gl_PointCoord.xy - vec2(0.5,0.5);
+    P = vec2(v_orientation.x*P.x - v_orientation.y*P.y,
+             v_orientation.y*P.x + v_orientation.x*P.y);
+    float point_size = SQRT_2*v_size  + 2. * (v_linewidth + 1.5*v_antialias);
+    float distance = marker_<marker>(P*point_size, v_size);
+    gl_FragColor = <paint>(distance, v_linewidth, v_antialias, v_fg_color, v_bg_color);
+}
diff --git a/vispy/glsl/collections/agg-marker.vert b/vispy/glsl/collections/agg-marker.vert
new file mode 100644
index 0000000..a09ce59
--- /dev/null
+++ b/vispy/glsl/collections/agg-marker.vert
@@ -0,0 +1,48 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <transform> : vec4 function(position, ...)
+//
+// ----------------------------------------------------------------------------
+#version 120
+#include "math/constants.glsl"
+
+// Collection externs
+// ------------------------------------
+// extern vec2  position;
+// extern float size;
+// extern vec4  fg_color;
+// extern vec4  bg_color;
+// extern float orientation;
+// extern float antialias;
+// extern float linewidth;
+
+
+// Varyings
+// ------------------------------------
+varying float v_size;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying vec2  v_orientation;
+varying float v_antialias;
+varying float v_linewidth;
+
+
+// Main (hooked)
+// ------------------------------------
+void main (void)
+{
+    fetch_uniforms();
+
+    v_size        = size;
+    v_linewidth   = linewidth;
+    v_antialias   = antialias;
+    v_fg_color    = fg_color;
+    v_bg_color    = bg_color;
+    v_orientation = vec2(cos(orientation), sin(orientation));
+
+    gl_Position = <transform>;
+    gl_PointSize = M_SQRT2 * size + 2.0 * (linewidth + 1.5*antialias);
+}
diff --git a/vispy/glsl/collections/agg-path.frag b/vispy/glsl/collections/agg-path.frag
new file mode 100644
index 0000000..7b25896
--- /dev/null
+++ b/vispy/glsl/collections/agg-path.frag
@@ -0,0 +1,55 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/antialias.glsl"
+#include "antialias/caps.glsl"
+
+// Varyings
+// ------------------------------------
+varying vec2  v_caps;
+varying vec4  v_color;
+varying float v_length;
+varying vec2  v_texcoord;
+varying float v_linewidth;
+varying float v_antialias;
+varying float v_miter_limit;
+varying vec2  v_bevel_distance;
+
+void main()
+{
+    float distance = v_texcoord.y;
+
+    if (v_caps.x < 0.0)
+    {
+        gl_FragColor = cap(1, v_texcoord.x, v_texcoord.y,
+                           v_linewidth, v_antialias, v_color);
+        // Do not return here or clipping won't be enforced
+        // return;
+    }
+    else if (v_caps.y > v_length)
+    {
+        gl_FragColor = cap(1, v_texcoord.x-v_length, v_texcoord.y,
+                           v_linewidth, v_antialias, v_color);
+        // Do not return here or clipping won't be enforced
+        // return;
+    }
+
+    // Round join (instead of miter)
+    // if (v_texcoord.x < 0.0)          { distance = length(v_texcoord); }
+    // else if(v_texcoord.x > v_length) { distance = length(v_texcoord - vec2(v_length, 0.0)); }
+
+    else {
+        // Miter limit
+        float t = (v_miter_limit-1.0)*(v_linewidth/2.0) + v_antialias;
+        if( (v_texcoord.x < 0.0) && (v_bevel_distance.x > (abs(distance) + t)) )
+        {
+            distance = v_bevel_distance.x - t;
+        }
+        else if( (v_texcoord.x > v_length) && (v_bevel_distance.y > (abs(distance) + t)) )
+        {
+            distance = v_bevel_distance.y - t;
+        }
+        gl_FragColor = stroke(distance, v_linewidth, v_antialias, v_color);
+    }
+}
diff --git a/vispy/glsl/collections/agg-path.vert b/vispy/glsl/collections/agg-path.vert
new file mode 100644
index 0000000..542b523
--- /dev/null
+++ b/vispy/glsl/collections/agg-path.vert
@@ -0,0 +1,166 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <transform> : vec4 function(position, ...)
+//
+// ----------------------------------------------------------------------------
+#include "misc/viewport-NDC.glsl"
+#include "math/point-to-line-distance.glsl"
+#include "math/point-to-line-projection.glsl"
+
+// Externs
+// ------------------------------------
+// extern vec3  p0;
+// extern vec3  p1;
+// extern vec3  p2;
+// extern vec3  p3;
+// extern vec2  uv;
+// extern vec2  caps;
+// extern vec4  color;
+// extern float antialias;
+// extern float linewidth;
+// extern float miter_limit;
+// extern vec4 viewport;
+// vec4 transform(vec3 position);
+
+
+// Varyings
+// ------------------------------------
+varying vec2  v_caps;
+varying vec4  v_color;
+
+varying float v_antialias;
+varying float v_linewidth;
+
+varying float v_length;
+varying vec2  v_texcoord;
+varying float v_miter_limit;
+varying vec2  v_bevel_distance;
+
+
+// Main
+// ------------------------------------
+void main (void)
+{
+    // This function is externally generated
+    fetch_uniforms();
+
+    v_color = color;
+    v_caps = caps;
+    v_linewidth = linewidth;
+    v_antialias = antialias;
+    v_miter_limit = miter_limit;
+
+    // transform prev/curr/next
+    vec4 p0_ = $transform(vec4(p0, 1));
+    vec4 p1_ = $transform(vec4(p1, 1));
+    vec4 p2_ = $transform(vec4(p2, 1));
+    vec4 p3_ = $transform(vec4(p3, 1));
+
+    // prev/curr/next in viewport coordinates
+    vec2 _p0 = NDC_to_viewport(p0_, viewport.zw);
+    vec2 _p1 = NDC_to_viewport(p1_, viewport.zw);
+    vec2 _p2 = NDC_to_viewport(p2_, viewport.zw);
+    vec2 _p3 = NDC_to_viewport(p3_, viewport.zw);
+
+    v_antialias = antialias;
+    v_linewidth = linewidth;
+    v_miter_limit = miter_limit;
+
+    // Determine the direction of each of the 3 segments (previous, current, next)
+    vec2 v0 = normalize(_p1 - _p0);
+    vec2 v1 = normalize(_p2 - _p1);
+    vec2 v2 = normalize(_p3 - _p2);
+
+
+    // Determine the normal of each of the 3 segments (previous, current, next)
+    vec2 n0 = vec2(-v0.y, v0.x);
+    vec2 n1 = vec2(-v1.y, v1.x);
+    vec2 n2 = vec2(-v2.y, v2.x);
+
+    // Determine miter lines by averaging the normals of the 2 segments
+    vec2 miter_a;
+    vec2 miter_b;
+    const float epsilon = 0.1;
+
+    // WARN: Here we test if v0 = -v1 relatively to epsilon
+    if( length(v0+v1) < epsilon ) {
+        miter_a = n1;
+    } else {
+        miter_a = normalize(n0 + n1); // miter at start of current segment
+    }
+
+    // WARN: Here we test if v1 = -v2 relatively to epsilon
+    if( length(v1+v2) < epsilon ) {
+        miter_b = n1;
+    } else {
+        miter_b = normalize(n1 + n2); // miter at end of current segment
+    }
+
+    // Determine the length of the miter by projecting it onto normal
+    vec2 p,v;
+    float d, z;
+    float w = linewidth/2.0 + 1.5*antialias;
+    v_length = length(_p2-_p1);
+    float m = miter_limit*linewidth/2.0;
+    float length_a = w / dot(miter_a, n1);
+    float length_b = w / dot(miter_b, n1);
+
+
+    // Angle between prev and current segment (sign only)
+    float d0 = +1.0;
+    if( (v0.x*v1.y - v0.y*v1.x) > 0. ) { d0 = -1.0;}
+
+    // Angle between current and next segment (sign only)
+    float d1 = +1.0;
+    if( (v1.x*v2.y - v1.y*v2.x) > 0. ) { d1 = -1.0; }
+
+    // Adjust vertex position
+    if (uv.x == -1.) {
+        z = p1_.z / p1_.w;
+
+        // Cap at start
+        if( p0 == p1 ) {
+            p = _p1 - w*v1 + uv.y* w*n1;
+            v_texcoord = vec2(-w, uv.y*w);
+            v_caps.x = v_texcoord.x;
+            // Regular join
+        } else {
+            p = _p1 + uv.y * length_a * miter_a;
+            v_texcoord = vec2(point_to_line_projection(_p1,_p2,p), uv.y*w);
+            v_caps.x = 1.0;
+        }
+        if( p2 == p3 ) {
+            v_caps.y = v_texcoord.x;
+        } else {
+            v_caps.y = 1.0;
+        }
+        v_bevel_distance.x = uv.y*d0*point_to_line_distance(_p1+d0*n0*w, _p1+d0*n1*w, p);
+        v_bevel_distance.y =        -point_to_line_distance(_p2+d1*n1*w, _p2+d1*n2*w, p);
+    } else {
+        z = p2_.z / p2_.w;
+
+        // Cap at end
+        if( p2 == p3 ) {
+            p = _p2 + w*v1 + uv.y*w*n1;
+            v_texcoord = vec2(v_length+w, uv.y*w);
+            v_caps.y = v_texcoord.x;
+        // Regular join
+        } else {
+            p = _p2 + uv.y*length_b * miter_b;
+            v_texcoord = vec2(point_to_line_projection(_p1,_p2,p), uv.y*w);
+            v_caps.y = 1.0;
+        }
+        if( p0 == p1 ) {
+            v_caps.x = v_texcoord.x;
+        } else {
+            v_caps.x = 1.0;
+        }
+        v_bevel_distance.x =        -point_to_line_distance(_p1+d0*n0*w, _p1+d0*n1*w, p);
+        v_bevel_distance.y = uv.y*d1*point_to_line_distance(_p2+d1*n1*w, _p2+d1*n2*w, p);
+    }
+
+    gl_Position = viewport_to_NDC(vec3(p,z), viewport.zw);
+}
diff --git a/vispy/glsl/collections/agg-point.frag b/vispy/glsl/collections/agg-point.frag
new file mode 100644
index 0000000..b3f5968
--- /dev/null
+++ b/vispy/glsl/collections/agg-point.frag
@@ -0,0 +1,21 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "markers/disc.glsl"
+#include "antialias/filled.glsl"
+
+// Varyings
+// ------------------------------------
+varying float v_size;
+varying vec4  v_color;
+
+// Main
+// ------------------------------------
+void main()
+{
+    vec2 P = gl_PointCoord.xy - vec2(0.5,0.5);
+    float point_size = v_size  + 2. * (1.0 + 1.5*1.0);
+    float distance = marker_disc(P*point_size, v_size);
+    gl_FragColor = filled(distance, 1.0, 1.0, v_color);
+}
diff --git a/vispy/glsl/collections/agg-point.vert b/vispy/glsl/collections/agg-point.vert
new file mode 100644
index 0000000..593192c
--- /dev/null
+++ b/vispy/glsl/collections/agg-point.vert
@@ -0,0 +1,35 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Externs
+// ------------------------------------
+// extern vec3 position;
+// extern float size;
+// extern vec4 fg_color;
+// extern vec4 bg_color;
+// extern float orientation;
+// extern float antialias;
+// extern float linewidth;
+// extern vec4 transform(vec3);
+
+// Varyings
+// ------------------------------------
+varying float v_size;
+varying vec4  v_color;
+varying float v_linewidth;
+varying float v_antialias;
+
+// Main (hooked)
+// ------------------------------------
+void main (void)
+{
+    fetch_uniforms();
+
+    v_size = size;
+    v_color = color;
+
+    gl_Position = $transform(vec4(position, 1));
+    gl_PointSize = size + 2.0 * (1.0 + 1.5*1.0);
+}
diff --git a/vispy/glsl/collections/agg-segment.frag b/vispy/glsl/collections/agg-segment.frag
new file mode 100644
index 0000000..b9779e7
--- /dev/null
+++ b/vispy/glsl/collections/agg-segment.frag
@@ -0,0 +1,32 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "antialias/caps.glsl"
+#include "antialias/antialias.glsl"
+
+// Varyings
+// ------------------------------------
+varying float v_length;
+varying float v_linewidth;
+varying float v_antialias;
+varying vec2  v_texcoord;
+varying vec4  v_color;
+
+
+// Main
+// ------------------------------------
+void main (void)
+{
+    if (v_texcoord.x < 0.0) {
+        gl_FragColor = cap( CAP_ROUND,
+                            v_texcoord.x, v_texcoord.y,
+                            v_linewidth, v_antialias, v_color);
+    } else if(v_texcoord.x > v_length) {
+        gl_FragColor = cap( CAP_ROUND,
+                            v_texcoord.x-v_length, v_texcoord.y,
+                            v_linewidth, v_antialias, v_color);
+    } else {
+        gl_FragColor = stroke(v_texcoord.y, v_linewidth, v_antialias, v_color);
+    }
+}
diff --git a/vispy/glsl/collections/agg-segment.vert b/vispy/glsl/collections/agg-segment.vert
new file mode 100644
index 0000000..f2d5b43
--- /dev/null
+++ b/vispy/glsl/collections/agg-segment.vert
@@ -0,0 +1,75 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <transform> : vec4 function(position, ...)
+//
+// ----------------------------------------------------------------------------
+#include "misc/viewport-NDC.glsl"
+
+// Externs
+// ------------------------------------
+// extern vec3  P0;
+// extern vec3  P1;
+// extern float index;
+// extern vec4  color;
+// extern float antialias;
+// extern float linewidth;
+// extern vec4  viewport;
+// vec4 transform(vec3 position);
+
+// Varyings
+// ------------------------------------
+varying float v_length;
+varying float v_antialias;
+varying float v_linewidth;
+varying vec2  v_texcoord;
+varying vec4  v_color;
+
+
+
+// Main
+// ------------------------------------
+void main (void)
+{
+    // This function is externally generated
+    fetch_uniforms();
+    v_linewidth = linewidth;
+    v_antialias = antialias;
+    v_color     = color;
+
+    vec4 P0_ = $transform(vec4(P0, 1));
+    vec4 P1_ = $transform(vec4(P1, 1));
+
+    // p0/p1 in viewport coordinates
+    vec2 p0 = NDC_to_viewport(P0_, viewport.zw);
+    vec2 p1 = NDC_to_viewport(P1_, viewport.zw);
+
+    //
+    vec2 position;
+    vec2 T = p1 - p0;
+    v_length = length(T);
+    float w = v_linewidth/2.0 + 1.5*v_antialias;
+    T = w*normalize(T);
+    float z;
+    if( index < 0.5 ) {
+       position = vec2( p0.x-T.y-T.x, p0.y+T.x-T.y);
+       v_texcoord = vec2(-w, +w);
+       z = P0.z;
+    } else if( index < 1.5 ) {
+       position = vec2(p0.x+T.y-T.x, p0.y-T.x-T.y);
+       v_texcoord= vec2(-w, -w);
+       z = P0.z;
+    } else if( index < 2.5 ) {
+       position = vec2( p1.x+T.y+T.x, p1.y-T.x+T.y);
+       v_texcoord= vec2(v_length+w, -w);
+       z = P1.z;
+    } else {
+       position = vec2( p1.x-T.y+T.x, p1.y+T.x+T.y);
+       v_texcoord = vec2(v_length+w, +w);
+       z = P1.z;
+    }
+
+    gl_Position = viewport_to_NDC(vec3(position,z), viewport.zw);
+}
diff --git a/vispy/glsl/collections/marker.frag b/vispy/glsl/collections/marker.frag
new file mode 100644
index 0000000..5f59eff
--- /dev/null
+++ b/vispy/glsl/collections/marker.frag
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <paint>  : "stroke", "filled" or "outline"
+//  <marker> : "arrow", "asterisk", "chevron", "clover", "club",
+//             "cross", "diamond", "disc", "ellipse", "hbar",
+//             "heart", "infinity", "pin", "ring", "spade",
+//             "square", "tag", "triangle", "vbar"
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+#include "markers/markers.glsl"
+#include "antialias/antialias.glsl"
+
+// Varyings
+// ------------------------------------
+varying float v_antialias;
+varying float v_linewidth;
+varying float v_size;
+varying float v_texcoord;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying vec2  v_orientation;
+
+// Main (hooked)
+// ------------------------------------
+void main()
+{
+    <viewport.clipping>;
+
+    vec2 P = gl_PointCoord.xy - vec2(0.5,0.5);
+    P = vec2(v_orientation.x*P.x - v_orientation.y*P.y,
+             v_orientation.y*P.x + v_orientation.x*P.y);
+    float point_size = M_SQRT2*v_size  + 2. * (v_linewidth + 1.5*v_antialias);
+    float distance = marker_<marker>(P*point_size, v_size);
+    gl_FragColor = <paint>(distance, v_linewidth, v_antialias, v_fg_color, v_bg_color);
+}
diff --git a/vispy/glsl/collections/marker.vert b/vispy/glsl/collections/marker.vert
new file mode 100644
index 0000000..6a4e9cc
--- /dev/null
+++ b/vispy/glsl/collections/marker.vert
@@ -0,0 +1,48 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <transform> : vec4 function(position)
+//
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+
+// Externs
+// ------------------------------------
+// attribute vec3  position;
+// attribute float size;
+// attribute vec4  fg_color;
+// attribute vec4  bg_color;
+// attribute float orientation;
+// attribute float linewidth;
+
+// Varyings
+// ------------------------------------
+varying float v_size;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying vec2  v_orientation;
+varying float v_antialias;
+varying float v_linewidth;
+
+// Main (hooked)
+// ------------------------------------
+void main (void)
+{
+    // From collection
+    fetch_uniforms();
+
+    v_size        = size;
+    v_linewidth   = linewidth;
+    v_antialias   = antialias;
+    v_fg_color    = fg_color;
+    v_bg_color    = bg_color;
+    v_orientation = vec2(cos(orientation), sin(orientation));
+
+    gl_Position = <transform(position)>;
+    gl_PointSize = M_SQRT2 * size + 2.0 * (linewidth + 1.5*antialias);
+
+    <viewport.transform>;
+}
diff --git a/vispy/glsl/collections/raw-path.frag b/vispy/glsl/collections/raw-path.frag
new file mode 100644
index 0000000..a1c87e0
--- /dev/null
+++ b/vispy/glsl/collections/raw-path.frag
@@ -0,0 +1,15 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Varyings
+// ------------------------------------
+varying vec4  v_color;
+
+// Main
+// ------------------------------------
+void main()
+{
+    gl_FragColor = v_color;
+}
diff --git a/vispy/glsl/collections/raw-path.vert b/vispy/glsl/collections/raw-path.vert
new file mode 100644
index 0000000..93b4607
--- /dev/null
+++ b/vispy/glsl/collections/raw-path.vert
@@ -0,0 +1,24 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Externs
+// ------------------------------------
+// extern vec3  position;
+// extern float id;
+// extern vec4  color;
+// vec4 transform(vec3 position);
+
+// Varyings
+// ------------------------------------
+varying vec4 v_color;
+
+// Main
+// ------------------------------------
+void main (void)
+{
+    fetch_uniforms();
+    v_color = vec4(color.rgb, color.a*id);
+    gl_Position = $transform(vec4(position, 1));
+}
diff --git a/vispy/glsl/collections/raw-point.frag b/vispy/glsl/collections/raw-point.frag
new file mode 100644
index 0000000..fa23bb7
--- /dev/null
+++ b/vispy/glsl/collections/raw-point.frag
@@ -0,0 +1,14 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Varyings
+// ------------------------------------
+varying float v_size;
+varying vec4  v_color;
+
+void main(void)
+{
+    gl_FragColor = v_color;
+}
diff --git a/vispy/glsl/collections/raw-point.vert b/vispy/glsl/collections/raw-point.vert
new file mode 100644
index 0000000..e5bf62d
--- /dev/null
+++ b/vispy/glsl/collections/raw-point.vert
@@ -0,0 +1,31 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Externs
+// ------------------------------------
+// extern vec3 position;
+// extern float size;
+// extern vec4 color;
+// vec4 transform(vec3 position);
+
+
+// Varyings
+// ------------------------------------
+varying float v_size;
+varying vec4  v_color;
+
+
+// Main (hooked)
+// ------------------------------------
+void main()
+{
+    fetch_uniforms();
+
+    v_size = size;
+    v_color = color;
+
+    gl_Position = $transform(vec4(position, 1));
+    gl_PointSize = size;
+}
diff --git a/vispy/glsl/collections/raw-segment.frag b/vispy/glsl/collections/raw-segment.frag
new file mode 100644
index 0000000..4d10ba5
--- /dev/null
+++ b/vispy/glsl/collections/raw-segment.frag
@@ -0,0 +1,18 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Varyings
+// ------------------------------------
+varying vec4  v_color;
+
+
+// Main
+// ------------------------------------
+void main (void)
+{
+    <viewport.clipping>;
+
+    gl_FragColor = v_color;
+}
diff --git a/vispy/glsl/collections/raw-segment.vert b/vispy/glsl/collections/raw-segment.vert
new file mode 100644
index 0000000..cc64316
--- /dev/null
+++ b/vispy/glsl/collections/raw-segment.vert
@@ -0,0 +1,26 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Externs
+// ------------------------------------
+// extern vec3 position;
+// extern vec4 color;
+// extern vec4 viewport;
+// vec4 transform(vec3 position);
+
+// Varyings
+// ------------------------------------
+varying vec4 v_color;
+
+// Main
+// ------------------------------------
+void main (void)
+{
+    // This function is externally generated
+    fetch_uniforms();
+    v_color = color;
+
+    gl_Position = $transform(vec4(position, 1));
+}
diff --git a/vispy/glsl/collections/raw-triangle.frag b/vispy/glsl/collections/raw-triangle.frag
new file mode 100644
index 0000000..bc4376f
--- /dev/null
+++ b/vispy/glsl/collections/raw-triangle.frag
@@ -0,0 +1,13 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Varyings
+// ------------------------------------
+varying vec4 v_color;
+
+void main(void)
+{
+    gl_FragColor = v_color;
+}
diff --git a/vispy/glsl/collections/raw-triangle.vert b/vispy/glsl/collections/raw-triangle.vert
new file mode 100644
index 0000000..79c8f1d
--- /dev/null
+++ b/vispy/glsl/collections/raw-triangle.vert
@@ -0,0 +1,26 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Externs
+// ------------------------------------
+// extern vec3 position;
+// extern float size;
+// extern vec4 color;
+// vec4 transform(vec3 position);
+
+
+// Varyings
+// ------------------------------------
+varying vec4  v_color;
+
+// Main
+// ------------------------------------
+void main()
+{
+    fetch_uniforms();
+    v_color = color;
+
+    gl_Position = $transform(vec4(position, 1));
+}
diff --git a/vispy/glsl/collections/sdf-glyph-ticks.vert b/vispy/glsl/collections/sdf-glyph-ticks.vert
new file mode 100644
index 0000000..5cb1d92
--- /dev/null
+++ b/vispy/glsl/collections/sdf-glyph-ticks.vert
@@ -0,0 +1,69 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+#include "misc/viewport-NDC.glsl"
+
+// Externs
+// ------------------------------------
+// vec2 position;
+// vec2 texcoord;
+// float scale;
+// vec3 origin;
+// vec3 direction
+// vec4 color;
+
+// Varyings
+// ------------------------------------
+varying float v_scale;
+varying vec2  v_texcoord;
+varying vec4  v_color;
+
+
+// Main
+// ------------------------------------
+void main()
+{
+    fetch_uniforms();
+
+    vec3 up = vec3(0,0,-1);
+
+    // Untransformed normalized tangent and orhogonal directions
+    vec3 tangent = normalize(direction.xyz);
+    vec3 ortho   = normalize(cross(tangent, up));
+
+    vec4 T = <transform((origin + scale*tangent))> - <transform(origin)>;
+    T = scale*normalize(T);
+
+    vec4 O = <transform((origin + scale*ortho))>   - <transform(origin)>;
+    O = scale*normalize(O);
+
+    vec4 P1_ = <transform(origin)> + ( position.x*T + position.y*O);
+    vec2 p1 = NDC_to_viewport(P1_, <viewport.viewport_global>.zw);
+/*
+    vec3 P1 = origin + scale*(tangent*position.x + ortho*position.y);
+    vec4 P1_ = <transform(P1)>;
+    vec2 p1 = NDC_to_viewport(P1_, <viewport.viewport_global>.zw);
+*/
+
+    // This compute an estimation of the actual size of the glyph
+    vec3 P2 = origin + scale*(tangent*(position.x+64.0) + ortho*(position.y));
+    vec4 P2_ = <transform(P2)>;
+    vec2 p2 = NDC_to_viewport(P2_, <viewport.viewport_global>.zw);
+
+    vec3 P3 = origin + scale*(tangent*(position.x) + ortho*(position.y+64.0));
+    vec4 P3_ = <transform(P3)>;
+    vec2 p3 = NDC_to_viewport(P3_, <viewport.viewport_global>.zw);
+
+    float d2 = length(p2 - p1);
+    float d3 = length(p3 - p1);
+    v_scale = min(d2,d3);
+
+
+    gl_Position = P1_;
+    v_texcoord = texcoord;
+    v_color = color;
+
+    <viewport.transform>;
+}
diff --git a/vispy/glsl/collections/sdf-glyph.frag b/vispy/glsl/collections/sdf-glyph.frag
new file mode 100644
index 0000000..3cd3459
--- /dev/null
+++ b/vispy/glsl/collections/sdf-glyph.frag
@@ -0,0 +1,80 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Ref: http://www.java-gaming.org/index.php?topic=33612.0
+//      http://www.reddit.com/
+//       -> r/gamedev/comments/2879jd/just_found_out_about_signed_distance_field_text/
+#include "math/constants.glsl"
+#include "misc/spatial-filters.frag"
+
+// Uniforms
+// ------------------------------------
+uniform sampler2D atlas_data;
+uniform vec2      atlas_shape;
+
+// Varyings
+// ------------------------------------
+varying float v_scale;
+varying vec2 v_texcoord;
+varying vec4 v_color;
+
+
+float contour(in float d, in float w)
+{
+    return smoothstep(0.5 - w, 0.5 + w, d);
+}
+float sample(sampler2D texture, vec2 uv, float w)
+{
+    return contour(texture2D(texture, uv).r, w);
+}
+
+
+// Main
+// ------------------------------------
+void main(void)
+{
+    <viewport.clipping>;
+
+    vec4 color = v_color;
+
+    // Retrieve distance from texture
+    float dist;
+    if(v_scale > 50.) {
+        dist = Bicubic(atlas_data, atlas_shape, v_texcoord).r;
+        // Debug
+        // color = vec4(0,0,1,1);
+    } else {
+        dist = texture2D(atlas_data, v_texcoord).r;
+    }
+
+
+    // fwidth helps keep outlines a constant width irrespective of scaling
+    // GLSL's fwidth = abs(dFdx(uv)) + abs(dFdy(uv))
+    float width = fwidth(dist);
+
+    // Regular SDF
+    float alpha = contour( dist, width );
+
+    // Supersampled version (when scale is small)
+    if (v_scale < 30.)
+    {
+        // Debug
+        // color = vec4(1,0,0,1);
+
+        // Supersample, 4 extra points
+        // Half of 1/sqrt2; you can play with this
+        float dscale = 0.5 * M_SQRT1_2;
+        vec2 duv = dscale * (dFdx(v_texcoord) + dFdy(v_texcoord));
+        vec4 box = vec4(v_texcoord-duv, v_texcoord+duv);
+        float asum = sample(atlas_data, box.xy, width)
+                   + sample(atlas_data, box.zw, width)
+                   + sample(atlas_data, box.xw, width)
+                   + sample(atlas_data, box.zy, width);
+
+        // weighted average, with 4 extra points having 0.5 weight each,
+        // so 1 + 0.5*4 = 3 is the divisor
+        alpha = (alpha + 0.5 * asum) / 3.0;
+    }
+    gl_FragColor = vec4(color.rgb, alpha*color.a);
+}
diff --git a/vispy/glsl/collections/sdf-glyph.vert b/vispy/glsl/collections/sdf-glyph.vert
new file mode 100644
index 0000000..7c0bef4
--- /dev/null
+++ b/vispy/glsl/collections/sdf-glyph.vert
@@ -0,0 +1,59 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+#include "misc/viewport-NDC.glsl"
+
+// Externs
+// ------------------------------------
+// vec2 position;
+// vec2 texcoord;
+// float scale;
+// vec3 origin;
+// vec3 direction
+// vec4 color;
+
+// Varyings
+// ------------------------------------
+varying float v_scale;
+varying vec2  v_texcoord;
+varying vec4  v_color;
+
+
+// Main
+// ------------------------------------
+void main()
+{
+    fetch_uniforms();
+
+    vec3 up = vec3(0,0,-1);
+
+    // Untransformed normalized tangent and orhogonal directions
+    vec3 tangent = normalize(direction.xyz);
+    vec3 ortho   = normalize(cross(tangent, up));
+
+    vec3 P1 = origin + scale*(tangent*position.x + ortho*position.y);
+    vec4 P1_ = <transform(P1)>;
+    vec2 p1 = NDC_to_viewport(P1_, <viewport.viewport_global>.zw);
+
+    // This compute an estimation of the actual size of the glyph
+    vec3 P2 = origin + scale*(tangent*(position.x+64.0) + ortho*(position.y));
+    vec4 P2_ = <transform(P2)>;
+    vec2 p2 = NDC_to_viewport(P2_, <viewport.viewport_global>.zw);
+
+    vec3 P3 = origin + scale*(tangent*(position.x) + ortho*(position.y+64.0));
+    vec4 P3_ = <transform(P3)>;
+    vec2 p3 = NDC_to_viewport(P3_, <viewport.viewport_global>.zw);
+
+    float d2 = length(p2 - p1);
+    float d3 = length(p3 - p1);
+    v_scale = min(d2,d3);
+
+
+    gl_Position = P1_;
+    v_texcoord = texcoord;
+    v_color = color;
+
+    <viewport.transform>;
+}
diff --git a/vispy/glsl/collections/tick-labels.vert b/vispy/glsl/collections/tick-labels.vert
new file mode 100644
index 0000000..184d6e0
--- /dev/null
+++ b/vispy/glsl/collections/tick-labels.vert
@@ -0,0 +1,71 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+#include "misc/viewport-NDC.glsl"
+
+// Externs
+// ------------------------------------
+// vec2 position;
+// vec2 texcoord;
+// float scale;
+// vec3 origin;
+// vec3 direction
+// vec4 color;
+
+// Varyings
+// ------------------------------------
+varying float v_scale;
+varying vec2  v_texcoord;
+varying vec4  v_color;
+
+
+// This works because we know the matplotlib API and how a transform is built
+float xscale(float x)        { return <transform.xscale.forward!(x)>; }
+float yscale(float y)        { return <transform.yscale.forward!(y)>; }
+float zscale(float z)        { return <transform.zscale.forward!(z)>; }
+vec3 data_projection(vec3 P) { return <transform.data_projection.forward!(P)>.xyz; }
+vec4 view_projection(vec3 P) { return <transform.view_projection.transform!(vec4(P,1))>; }
+
+vec3 data_scale(vec3 P)      { return vec3(xscale(P.x), yscale(P.y), zscale(P.z)); }
+vec4 transform(vec3 P)       { return view_projection(data_projection(data_scale(P))); }
+
+
+// Main
+// ------------------------------------
+void main()
+{
+    fetch_uniforms();
+
+    vec3 up = vec3(0,0,-1);
+
+    vec3 O = data_projection(data_scale(origin));
+
+    vec3 tangent = normalize(data_projection(data_scale(origin+scale*direction)) - O);
+    vec3 ortho = normalize(cross(tangent, up));
+
+    vec3 P1 = O + scale*(position.x*tangent + position.y*ortho);
+    vec4 P1_ = view_projection(P1);
+    vec2 p1 = NDC_to_viewport(P1_, <viewport.viewport_global>.zw);
+
+    // This compute an estimation of the actual size of the glyph
+    vec3 P2 = O + scale*(tangent*(position.x+64.0) + ortho*(position.y));
+    vec4 P2_ = view_projection(P2);
+    vec2 p2 = NDC_to_viewport(P2_, <viewport.viewport_global>.zw);
+
+    vec3 P3 = O + scale*(tangent*(position.x) + ortho*(position.y+64.0));
+    vec4 P3_ = view_projection(P3);
+    vec2 p3 = NDC_to_viewport(P3_, <viewport.viewport_global>.zw);
+
+    float d2 = length(p2 - p1);
+    float d3 = length(p3 - p1);
+    v_scale = min(d2,d3);
+
+
+    gl_Position = P1_;
+    v_texcoord = texcoord;
+    v_color = color;
+
+    <viewport.transform>;
+}
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/glsl/colormaps/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/glsl/colormaps/__init__.py
diff --git a/vispy/glsl/colormaps/autumn.glsl b/vispy/glsl/colormaps/autumn.glsl
new file mode 100644
index 0000000..9bda5f2
--- /dev/null
+++ b/vispy/glsl/colormaps/autumn.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_autumn(float t)
+{
+    return mix(vec3(1.0,0.0,0.0), vec3(1.0,1.0,0.0), t);
+}
+
+vec3 colormap_autumn(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_autumn(t), under, over);
+}
+
+vec4 colormap_autumn(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_autumn(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/blues.glsl b/vispy/glsl/colormaps/blues.glsl
new file mode 100644
index 0000000..feef433
--- /dev/null
+++ b/vispy/glsl/colormaps/blues.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_blues(float t)
+{
+    return mix(vec3(1,1,1), vec3(0,0,1), t);
+}
+
+vec3 colormap_blues(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_blues(t), under, over);
+}
+
+vec4 colormap_blues(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_blues(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/color-space.glsl b/vispy/glsl/colormaps/color-space.glsl
new file mode 100644
index 0000000..45601cb
--- /dev/null
+++ b/vispy/glsl/colormaps/color-space.glsl
@@ -0,0 +1,17 @@
+vec3 hsv_to_rgb(vec3 c)
+{
+    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+
+vec3 rgb_to_hsv(vec3 c)
+{
+    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+
+    float d = q.x - min(q.w, q.y);
+    float e = 1.0e-10;
+    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
+}
diff --git a/vispy/glsl/colormaps/colormaps.glsl b/vispy/glsl/colormaps/colormaps.glsl
new file mode 100644
index 0000000..2846d32
--- /dev/null
+++ b/vispy/glsl/colormaps/colormaps.glsl
@@ -0,0 +1,24 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+#include "colormaps/user.glsl"
+
+#include "colormaps/hot.glsl"
+#include "colormaps/gray.glsl"
+#include "colormaps/cool.glsl"
+#include "colormaps/wheel.glsl"
+
+#include "colormaps/autumn.glsl"
+#include "colormaps/winter.glsl"
+#include "colormaps/spring.glsl"
+#include "colormaps/summer.glsl"
+
+#include "colormaps/ice.glsl"
+#include "colormaps/fire.glsl"
+#include "colormaps/icefire.glsl"
+
+#include "colormaps/reds.glsl"
+#include "colormaps/blues.glsl"
+#include "colormaps/greens.glsl"
diff --git a/vispy/glsl/colormaps/cool.glsl b/vispy/glsl/colormaps/cool.glsl
new file mode 100644
index 0000000..e4885cc
--- /dev/null
+++ b/vispy/glsl/colormaps/cool.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_cool(float t)
+{
+    return mix(vec3(0.0,1.0,1.0), vec3(1.0,0.0,1.0), t);
+}
+
+vec3 colormap_cool(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_cool(t), under, over);
+}
+
+vec4 colormap_cool(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_cool(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/fire.glsl b/vispy/glsl/colormaps/fire.glsl
new file mode 100644
index 0000000..73244d6
--- /dev/null
+++ b/vispy/glsl/colormaps/fire.glsl
@@ -0,0 +1,21 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_fire(float t)
+{
+    return mix(mix(vec3(1,1,1), vec3(1,1,0), t),
+               mix(vec3(1,1,0), vec3(1,0,0), t*t), t);
+}
+
+vec3 colormap_fire(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_fire(t), under, over);
+}
+
+vec4 colormap_fire(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_fire(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/gray.glsl b/vispy/glsl/colormaps/gray.glsl
new file mode 100644
index 0000000..0b8a30b
--- /dev/null
+++ b/vispy/glsl/colormaps/gray.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_gray(float t)
+{
+    return vec3(t);
+}
+
+vec3 colormap_gray(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_gray(t), under, over);
+}
+
+vec4 colormap_gray(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_gray(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/greens.glsl b/vispy/glsl/colormaps/greens.glsl
new file mode 100644
index 0000000..b11a41a
--- /dev/null
+++ b/vispy/glsl/colormaps/greens.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_greens(float t)
+{
+    return mix(vec3(1,1,1), vec3(0,1,0), t);
+}
+
+vec3 colormap_greens(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_greens(t), under, over);
+}
+
+vec4 colormap_greens(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_greens(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/hot.glsl b/vispy/glsl/colormaps/hot.glsl
new file mode 100644
index 0000000..c29d24a
--- /dev/null
+++ b/vispy/glsl/colormaps/hot.glsl
@@ -0,0 +1,22 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_hot(float t)
+{
+    return vec3(smoothstep(0.0,    1.0/3.0,t),
+                smoothstep(1.0/3.0,2.0/3.0,t),
+                smoothstep(2.0/3.0,1.0,    t));
+}
+
+vec3 colormap_hot(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_hot(t), under, over);
+}
+
+vec4 colormap_hot(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_hot(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/ice.glsl b/vispy/glsl/colormaps/ice.glsl
new file mode 100644
index 0000000..e17ac57
--- /dev/null
+++ b/vispy/glsl/colormaps/ice.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_ice(float t)
+{
+   return vec3(t, t, 1.0);
+}
+
+vec3 colormap_ice(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_ice(t), under, over);
+}
+
+vec4 colormap_ice(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_ice(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/icefire.glsl b/vispy/glsl/colormaps/icefire.glsl
new file mode 100644
index 0000000..3c889ca
--- /dev/null
+++ b/vispy/glsl/colormaps/icefire.glsl
@@ -0,0 +1,23 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+#include "colormaps/ice.glsl"
+#include "colormaps/fire.glsl"
+
+vec3 colormap_icefire(float t)
+{
+    return colormap_segment(0.0,0.5,t) * colormap_ice(2.0*(t-0.0)) +
+           colormap_segment(0.5,1.0,t) * colormap_fire(2.0*(t-0.5));
+}
+
+vec3 colormap_icefire(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_icefire(t), under, over);
+}
+
+vec4 colormap_icefire(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_icefire(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/parse.py b/vispy/glsl/colormaps/parse.py
new file mode 100644
index 0000000..563acb8
--- /dev/null
+++ b/vispy/glsl/colormaps/parse.py
@@ -0,0 +1,38 @@
+import os
+import re
+
+
+def get(filename):
+    for path in ["..", "."]:
+        filepath = os.path.join(path, filename)
+        if os.path.exists(filepath):
+            with open(filepath) as infile:
+                code = infile.read()
+                # comment = '#line 0 // Start of "%s"\n' % filename
+                comment = '// --- start of "%s" ---\n' % filename
+            return comment + code
+    return '#error "%s" not found !\n' % filename
+
+code = """
+#include "colormap/colormaps.glsl"
+"""
+
+re_include = re.compile('\#include\s*"(?P<filename>[a-zA-Z0-9\-\.\/]+)"')
+
+includes = []
+
+
+def replace(match):
+    filename = match.group("filename")
+    if filename not in includes:
+        includes.append(filename)
+        text = get(filename)
+        # lineno = code.count("\n",0,match.start())+1
+        # text += '\n#line %d // End of "%s"' % (lineno, filename)
+        text += '// --- end of "%s" ---\n' % filename
+        return text
+    return ''
+
+while re.search(re_include, code):
+    code = re.sub(re_include, replace, code)
+print(code)
diff --git a/vispy/glsl/colormaps/reds.glsl b/vispy/glsl/colormaps/reds.glsl
new file mode 100644
index 0000000..50b28bf
--- /dev/null
+++ b/vispy/glsl/colormaps/reds.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_reds(float t)
+{
+    return mix(vec3(1,1,1), vec3(1,0,0), t);
+}
+
+vec3 colormap_reds(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_reds(t), under, over);
+}
+
+vec4 colormap_reds(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_reds(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/spring.glsl b/vispy/glsl/colormaps/spring.glsl
new file mode 100644
index 0000000..1eebc65
--- /dev/null
+++ b/vispy/glsl/colormaps/spring.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_spring(float t)
+{
+    return mix(vec3(1.0,0.0,1.0), vec3(1.0,1.0,0.0), t);
+}
+
+vec3 colormap_spring(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_spring(t), under, over);
+}
+
+vec4 colormap_spring(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_spring(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/summer.glsl b/vispy/glsl/colormaps/summer.glsl
new file mode 100644
index 0000000..e38e254
--- /dev/null
+++ b/vispy/glsl/colormaps/summer.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_summer(float t)
+{
+    return mix(vec3(0.0,0.5,0.4), vec3(1.0,1.0,0.4), t);
+}
+
+vec3 colormap_summer(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_summer(t), under, over);
+}
+
+vec4 colormap_summer(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_summer(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/user.glsl b/vispy/glsl/colormaps/user.glsl
new file mode 100644
index 0000000..21fa9e4
--- /dev/null
+++ b/vispy/glsl/colormaps/user.glsl
@@ -0,0 +1,22 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+uniform sampler1D colormap;
+
+vec3 colormap_user(float t)
+{
+    return texture1D(colormap, t).rgb;
+}
+
+vec3 colormap_user(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_user(t), under, over);
+}
+
+vec4 colormap_user(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_user(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/util.glsl b/vispy/glsl/colormaps/util.glsl
new file mode 100644
index 0000000..c2c136f
--- /dev/null
+++ b/vispy/glsl/colormaps/util.glsl
@@ -0,0 +1,41 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/*
+ * t <= 0    : return 0
+ * 0 < t < 1 : return t
+ * t >= 1    : return 0
+ */
+float
+colormap_segment(float edge0, float edge1, float x)
+{
+    return step(edge0,x) * (1.0-step(edge1,x));
+}
+
+/*
+ * t <= 0    : return under
+ * 0 < t < 1 : return color
+ * t >= 1    : return over
+ */
+vec3
+colormap_underover(float t, vec3 color, vec3 under, vec3 over)
+{
+    return step(t,0.0)*under +
+           colormap_segment(0.0,1.0,t)*color +
+           step(1.0,t)*over;
+}
+
+/*
+ * t <= 0    : return under
+ * 0 < t < 1 : return color
+ * t >= 1    : return over
+ */
+vec4
+colormap_underover(float t, vec4 color, vec4 under, vec4 over)
+{
+    return step(t,0.0)*under +
+           colormap_segment(0.0,1.0,t)*color +
+           step(1.0,t)*over;
+}
diff --git a/vispy/glsl/colormaps/wheel.glsl b/vispy/glsl/colormaps/wheel.glsl
new file mode 100644
index 0000000..91f266a
--- /dev/null
+++ b/vispy/glsl/colormaps/wheel.glsl
@@ -0,0 +1,21 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+// Wheel colormap by Morgan McGuire
+vec3 colormap_wheel(float t)
+{
+    return clamp(abs(fract(t + vec3(1.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - 3.0) -1.0, 0.0, 1.0);
+}
+
+vec3 colormap_wheel(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_wheel(t), under, over);
+}
+
+vec4 colormap_wheel(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_wheel(t),1.0), under, over);
+}
diff --git a/vispy/glsl/colormaps/winter.glsl b/vispy/glsl/colormaps/winter.glsl
new file mode 100644
index 0000000..4cbf2aa
--- /dev/null
+++ b/vispy/glsl/colormaps/winter.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "colormaps/util.glsl"
+
+vec3 colormap_winter(float t)
+{
+    return mix(vec3(0.0,0.0,1.0), vec3(0.0,1.0,0.5), sqrt(t));
+}
+
+vec3 colormap_winter(float t, vec3 under, vec3 over)
+{
+    return colormap_underover(t, colormap_winter(t), under, over);
+}
+
+vec4 colormap_winter(float t, vec4 under, vec4 over)
+{
+    return colormap_underover(t, vec4(colormap_winter(t),1.0), under, over);
+}
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/glsl/markers/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/glsl/markers/__init__.py
diff --git a/vispy/glsl/markers/arrow.glsl b/vispy/glsl/markers/arrow.glsl
new file mode 100644
index 0000000..0a5ec82
--- /dev/null
+++ b/vispy/glsl/markers/arrow.glsl
@@ -0,0 +1,12 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+float marker_arrow(vec2 P, float size)
+{
+    float r1 = abs(P.x) + abs(P.y) - size/2;
+    float r2 = max(abs(P.x+size/2), abs(P.y)) - size/2;
+    float r3 = max(abs(P.x-size/6)-size/4, abs(P.y)- size/4);
+    return min(r3,max(.75*r1,r2));
+}
diff --git a/vispy/glsl/markers/asterisk.glsl b/vispy/glsl/markers/asterisk.glsl
new file mode 100644
index 0000000..6398ff9
--- /dev/null
+++ b/vispy/glsl/markers/asterisk.glsl
@@ -0,0 +1,16 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_asterisk(vec2 P, float size)
+{
+    float x = M_SQRT2/2 * (P.x - P.y);
+    float y = M_SQRT2/2 * (P.x + P.y);
+    float r1 = max(abs(x)- size/2, abs(y)- size/10);
+    float r2 = max(abs(y)- size/2, abs(x)- size/10);
+    float r3 = max(abs(P.x)- size/2, abs(P.y)- size/10);
+    float r4 = max(abs(P.y)- size/2, abs(P.x)- size/10);
+    return min( min(r1,r2), min(r3,r4));
+}
diff --git a/vispy/glsl/markers/chevron.glsl b/vispy/glsl/markers/chevron.glsl
new file mode 100644
index 0000000..ffa1c15
--- /dev/null
+++ b/vispy/glsl/markers/chevron.glsl
@@ -0,0 +1,14 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_chevron(vec2 P, float size)
+{
+    float x = 1.0/M_SQRT2 * ((P.x-size/6) - P.y);
+    float y = 1.0/M_SQRT2 * ((P.x-size/6) + P.y);
+    float r1 = max(abs(x),          abs(y))          - size/3.0;
+    float r2 = max(abs(x-size/3.0), abs(y-size/3.0)) - size/3.0;
+    return max(r1,-r2);
+}
diff --git a/vispy/glsl/markers/clover.glsl b/vispy/glsl/markers/clover.glsl
new file mode 100644
index 0000000..d26a3f9
--- /dev/null
+++ b/vispy/glsl/markers/clover.glsl
@@ -0,0 +1,20 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_clover(vec2 P, float size)
+{
+    const float t1 = -M_PI/2;
+    const vec2  c1 = 0.25*vec2(cos(t1),sin(t1));
+    const float t2 = t1+2*M_PI/3;
+    const vec2  c2 = 0.25*vec2(cos(t2),sin(t2));
+    const float t3 = t2+2*M_PI/3;
+    const vec2  c3 = 0.25*vec2(cos(t3),sin(t3));
+
+    float r1 = length( P - c1*size) - size/3.5;
+    float r2 = length( P - c2*size) - size/3.5;
+    float r3 = length( P - c3*size) - size/3.5;
+    return min(min(r1,r2),r3);
+}
diff --git a/vispy/glsl/markers/club.glsl b/vispy/glsl/markers/club.glsl
new file mode 100644
index 0000000..88200c4
--- /dev/null
+++ b/vispy/glsl/markers/club.glsl
@@ -0,0 +1,31 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_club(vec2 P, float size)
+{
+    // clover (3 discs)
+    const float t1 = -M_PI/2.0;
+    const vec2  c1 = 0.225*vec2(cos(t1),sin(t1));
+    const float t2 = t1+2*M_PI/3.0;
+    const vec2  c2 = 0.225*vec2(cos(t2),sin(t2));
+    const float t3 = t2+2*M_PI/3.0;
+    const vec2  c3 = 0.225*vec2(cos(t3),sin(t3));
+    float r1 = length( P - c1*size) - size/4.25;
+    float r2 = length( P - c2*size) - size/4.25;
+    float r3 = length( P - c3*size) - size/4.25;
+    float r4 =  min(min(r1,r2),r3);
+
+    // Root (2 circles and 2 planes)
+    const vec2 c4 = vec2(+0.65, 0.125);
+    const vec2 c5 = vec2(-0.65, 0.125);
+    float r5 = length(P-c4*size) - size/1.6;
+    float r6 = length(P-c5*size) - size/1.6;
+    float r7 = P.y - 0.5*size;
+    float r8 = 0.2*size - P.y;
+    float r9 = max(-min(r5,r6), max(r7,r8));
+
+    return min(r4,r9);
+}
diff --git a/vispy/glsl/markers/cross.glsl b/vispy/glsl/markers/cross.glsl
new file mode 100644
index 0000000..5ea4734
--- /dev/null
+++ b/vispy/glsl/markers/cross.glsl
@@ -0,0 +1,17 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_cross(vec2 P, float size)
+{
+    float x = M_SQRT2/2.0 * (P.x - P.y);
+    float y = M_SQRT2/2.0 * (P.x + P.y);
+    float r1 = max(abs(x - size/3.0), abs(x + size/3.0));
+    float r2 = max(abs(y - size/3.0), abs(y + size/3.0));
+    float r3 = max(abs(x), abs(y));
+    float r = max(min(r1,r2),r3);
+    r -= size/2;
+    return r;
+}
diff --git a/vispy/glsl/markers/diamond.glsl b/vispy/glsl/markers/diamond.glsl
new file mode 100644
index 0000000..083f0e6
--- /dev/null
+++ b/vispy/glsl/markers/diamond.glsl
@@ -0,0 +1,12 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_diamond(vec2 P, float size)
+{
+   float x = M_SQRT2/2.0 * (P.x - P.y);
+   float y = M_SQRT2/2.0 * (P.x + P.y);
+   return max(abs(x), abs(y)) - size/(2.0*M_SQRT2);
+}
diff --git a/vispy/glsl/markers/disc.glsl b/vispy/glsl/markers/disc.glsl
new file mode 100644
index 0000000..756d342
--- /dev/null
+++ b/vispy/glsl/markers/disc.glsl
@@ -0,0 +1,9 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+float marker_disc(vec2 P, float size)
+{
+    return length(P) - size/2;
+}
diff --git a/vispy/glsl/markers/ellipse.glsl b/vispy/glsl/markers/ellipse.glsl
new file mode 100644
index 0000000..54530f1
--- /dev/null
+++ b/vispy/glsl/markers/ellipse.glsl
@@ -0,0 +1,67 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+// --- ellipse
+// Created by Inigo Quilez - iq/2013
+// License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
+float marker_ellipse(vec2 P, float size)
+{
+    // Alternate version (approximation)
+    float a = 1.0;
+    float b = 2.0;
+    float r = 0.5*size;
+    float f = length( P*vec2(a,b) );
+    f = length( P*vec2(a,b) );
+    f = f*(f-r)/length( P*vec2(a*a,b*b) );
+    return f;
+
+/*
+    vec2 ab = vec2(size/3.0, size/2.0);
+    vec2 p = abs( P );
+    if( p.x > p.y ){
+        p = p.yx;
+        ab = ab.yx;
+    }
+    float l = ab.y*ab.y - ab.x*ab.x;
+    float m = ab.x*p.x/l;
+    float n = ab.y*p.y/l;
+    float m2 = m*m;
+    float n2 = n*n;
+
+    float c = (m2 + n2 - 1.0)/3.0;
+    float c3 = c*c*c;
+
+    float q = c3 + m2*n2*2.0;
+    float d = c3 + m2*n2;
+    float g = m + m*n2;
+
+    float co;
+
+    if(d < 0.0)
+    {
+        float p = acos(q/c3)/3.0;
+        float s = cos(p);
+        float t = sin(p)*sqrt(3.0);
+        float rx = sqrt( -c*(s + t + 2.0) + m2 );
+        float ry = sqrt( -c*(s - t + 2.0) + m2 );
+        co = ( ry + sign(l)*rx + abs(g)/(rx*ry) - m)/2.0;
+    }
+    else
+    {
+        float h = 2.0*m*n*sqrt( d );
+        float s = sign(q+h)*pow( abs(q+h), 1.0/3.0 );
+        float u = sign(q-h)*pow( abs(q-h), 1.0/3.0 );
+        float rx = -s - u - c*4.0 + 2.0*m2;
+        float ry = (s - u)*sqrt(3.0);
+        float rm = sqrt( rx*rx + ry*ry );
+        float p = ry/sqrt(rm-rx);
+        co = (p + 2.0*g/rm - m)/2.0;
+    }
+
+    float si = sqrt(1.0 - co*co);
+    vec2 closestPoint = vec2(ab.x*co, ab.y*si);
+    return length(closestPoint - p ) * sign(p.y-closestPoint.y);
+*/
+}
diff --git a/vispy/glsl/markers/hbar.glsl b/vispy/glsl/markers/hbar.glsl
new file mode 100644
index 0000000..6d3da27
--- /dev/null
+++ b/vispy/glsl/markers/hbar.glsl
@@ -0,0 +1,9 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+float marker_hbar(vec2 P, float size)
+{
+    return max(abs(P.x)- size/6.0, abs(P.y)- size/2.0);
+}
diff --git a/vispy/glsl/markers/heart.glsl b/vispy/glsl/markers/heart.glsl
new file mode 100644
index 0000000..9f46395
--- /dev/null
+++ b/vispy/glsl/markers/heart.glsl
@@ -0,0 +1,15 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_heart(vec2 P, float size)
+{
+   float x = M_SQRT2/2.0 * (P.x - P.y);
+   float y = M_SQRT2/2.0 * (P.x + P.y);
+   float r1 = max(abs(x),abs(y))-size/3.5;
+   float r2 = length(P - M_SQRT2/2.0*vec2(+1.0,-1.0)*size/3.5) - size/3.5;
+   float r3 = length(P - M_SQRT2/2.0*vec2(-1.0,-1.0)*size/3.5) - size/3.5;
+   return min(min(r1,r2),r3);
+}
diff --git a/vispy/glsl/markers/infinity.glsl b/vispy/glsl/markers/infinity.glsl
new file mode 100644
index 0000000..e5b7ef3
--- /dev/null
+++ b/vispy/glsl/markers/infinity.glsl
@@ -0,0 +1,15 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+float marker_infinity(vec2 P, float size)
+{
+    const vec2 c1 = vec2(+0.2125, 0.00);
+    const vec2 c2 = vec2(-0.2125, 0.00);
+    float r1 = length(P-c1*size) - size/3.5;
+    float r2 = length(P-c1*size) - size/7.5;
+    float r3 = length(P-c2*size) - size/3.5;
+    float r4 = length(P-c2*size) - size/7.5;
+    return min( max(r1,-r2), max(r3,-r4));
+}
diff --git a/vispy/glsl/markers/marker-sdf.frag b/vispy/glsl/markers/marker-sdf.frag
new file mode 100644
index 0000000..875f7e0
--- /dev/null
+++ b/vispy/glsl/markers/marker-sdf.frag
@@ -0,0 +1,74 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#version 120
+
+// Constants
+// ------------------------------------
+const float SQRT_2 = 1.4142135623730951;
+
+// Uniforms
+// ------------------------------------
+uniform sampler2D u_texture;
+uniform vec2 u_texture_shape;
+
+// Varyings
+// ------------------------------------
+varying float v_antialias;
+varying float v_linewidth;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying float v_size;
+varying vec2  v_rotation;
+
+vec4 Nearest(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Bilinear(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Hanning(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Hamming(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Hermite(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Kaiser(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Quadric(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Bicubic(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 CatRom(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Mitchell(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Spline16(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Spline36(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Gaussian(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Bessel(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Sinc(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Lanczos(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+vec4 Blackman(sampler2D u_data, vec2 u_shape, vec2 v_texcoord);
+
+void main()
+{
+    vec2 P = gl_PointCoord.xy - vec2(0.5,0.5);
+    P = vec2(v_rotation.x*P.x - v_rotation.y*P.y,
+             v_rotation.y*P.x + v_rotation.x*P.y);
+    P += vec2(0.5,0.5);
+
+    float r = v_size + 2*(v_linewidth + 1.5*v_antialias);
+    // float signed_distance = r * (texture2D(u_texture, P).r - 0.5);
+    float signed_distance = r * (Bicubic(u_texture, u_texture_shape, P).r - 0.5);
+    float t = v_linewidth/2.0 - v_antialias;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/v_antialias;
+    alpha = exp(-alpha*alpha);
+
+
+    // Within linestroke
+    if( border_distance < 0 )
+        gl_FragColor = v_fg_color;
+    else if( signed_distance < 0 )
+        // Inside shape
+        if( border_distance > (v_linewidth/2.0 + v_antialias) )
+            gl_FragColor = v_bg_color;
+        else // Line stroke interior border
+            gl_FragColor = mix(v_bg_color,v_fg_color,alpha);
+    else
+        // Outide shape
+        if( border_distance > (v_linewidth/2.0 + v_antialias) )
+            discard;
+        else // Line stroke exterior border
+            gl_FragColor = vec4(v_fg_color.rgb, v_fg_color.a * alpha);
+}
diff --git a/vispy/glsl/markers/marker-sdf.vert b/vispy/glsl/markers/marker-sdf.vert
new file mode 100644
index 0000000..1cdf79a
--- /dev/null
+++ b/vispy/glsl/markers/marker-sdf.vert
@@ -0,0 +1,41 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#version 120
+
+// Uniform
+// ------------------------------------
+uniform mat4  u_projection;
+uniform float u_antialias;
+
+// Attributes
+// ------------------------------------
+attribute float a_size;
+attribute float a_orientation;
+attribute float a_linewidth;
+attribute vec3  a_position;
+attribute vec4  a_fg_color;
+attribute vec4  a_bg_color;
+
+// Varyings
+// ------------------------------------
+varying float v_antialias;
+varying float v_linewidth;
+varying float v_size;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying vec2  v_rotation;
+
+void main (void)
+{
+    v_size = a_size;
+    v_linewidth = 2.5*a_linewidth;
+    v_antialias = 3.0*u_antialias;
+    v_fg_color = a_fg_color;
+    v_bg_color = a_bg_color;
+    v_rotation = vec2(cos(a_orientation), sin(a_orientation));
+
+    gl_Position = u_projection * vec4(a_position, 1.0);
+    gl_PointSize = a_size + 2*(a_linewidth + 1.5*v_antialias);
+}
diff --git a/vispy/glsl/markers/marker.frag b/vispy/glsl/markers/marker.frag
new file mode 100644
index 0000000..b73d70a
--- /dev/null
+++ b/vispy/glsl/markers/marker.frag
@@ -0,0 +1,36 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <paint>  : "stroke", "filled" or "outline"
+//  <marker> : "arrow", "asterisk", "chevron", "clover", "club",
+//             "cross", "diamond", "disc", "ellipse", "hbar",
+//             "heart", "infinity", "pin", "ring", "spade",
+//             "square", "tag", "triangle", "vbar"
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+#include "markers/markers.glsl"
+#include "antialias/antialias.glsl"
+
+// Varyings
+// ------------------------------------
+varying float v_antialias;
+varying float v_linewidth;
+varying float v_size;
+varying float v_texcoord;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying vec2  v_orientation;
+
+// Main (hooked)
+// ------------------------------------
+void main()
+{
+    vec2 P = gl_PointCoord.xy - vec2(0.5,0.5);
+    P = vec2(v_orientation.x*P.x - v_orientation.y*P.y,
+             v_orientation.y*P.x + v_orientation.x*P.y);
+    float point_size = M_SQRT2*v_size  + 2 * (v_linewidth + 1.5*v_antialias);
+    float distance = marker_<marker>(P*point_size, v_size);
+    gl_FragColor = <paint>(distance, v_linewidth, v_antialias, v_fg_color, v_bg_color);
+}
diff --git a/vispy/glsl/markers/marker.vert b/vispy/glsl/markers/marker.vert
new file mode 100644
index 0000000..3a3d922
--- /dev/null
+++ b/vispy/glsl/markers/marker.vert
@@ -0,0 +1,46 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+// Hooks:
+//  <transform> : vec4 function(position, ...)
+//
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+// Uniforms
+// ------------------------------------
+uniform float antialias;
+
+// Attributes
+// ------------------------------------
+attribute vec2  position;
+attribute float size;
+attribute vec4  fg_color;
+attribute vec4  bg_color;
+attribute float orientation;
+attribute float linewidth;
+
+// Varyings
+// ------------------------------------
+varying float v_size;
+varying vec4  v_fg_color;
+varying vec4  v_bg_color;
+varying vec2  v_orientation;
+varying float v_antialias;
+varying float v_linewidth;
+
+// Main (hooked)
+// ------------------------------------
+void main (void)
+{
+    v_size        = size;
+    v_linewidth   = linewidth;
+    v_antialias   = antialias;
+    v_fg_color    = fg_color;
+    v_bg_color    = bg_color;
+    v_orientation = vec2(cos(orientation), sin(orientation));
+
+    gl_Position = <transform>;
+    gl_PointSize = M_SQRT2 * size + 2.0 * (linewidth + 1.5*antialias);
+}
diff --git a/vispy/glsl/markers/markers.glsl b/vispy/glsl/markers/markers.glsl
new file mode 100644
index 0000000..1ccc3d7
--- /dev/null
+++ b/vispy/glsl/markers/markers.glsl
@@ -0,0 +1,24 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+#include "markers/arrow.glsl"
+#include "markers/asterisk.glsl"
+#include "markers/chevron.glsl"
+#include "markers/clover.glsl"
+#include "markers/club.glsl"
+#include "markers/cross.glsl"
+#include "markers/diamond.glsl"
+#include "markers/disc.glsl"
+#include "markers/ellipse.glsl"
+#include "markers/hbar.glsl"
+#include "markers/heart.glsl"
+#include "markers/infinity.glsl"
+#include "markers/pin.glsl"
+#include "markers/ring.glsl"
+#include "markers/spade.glsl"
+#include "markers/square.glsl"
+#include "markers/tag.glsl"
+#include "markers/triangle.glsl"
+#include "markers/vbar.glsl"
diff --git a/vispy/glsl/markers/pin.glsl b/vispy/glsl/markers/pin.glsl
new file mode 100644
index 0000000..16ff34f
--- /dev/null
+++ b/vispy/glsl/markers/pin.glsl
@@ -0,0 +1,18 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+float marker_pin(vec2 P, float size)
+{
+    size *= .9;
+
+    vec2 c1 = vec2(0.0,-0.15)*size;
+    float r1 = length(P-c1)-size/2.675;
+    vec2 c2 = vec2(+1.49,-0.80)*size;
+    float r2 = length(P-c2) - 2.*size;
+    vec2 c3 = vec2(-1.49,-0.80)*size;
+    float r3 = length(P-c3) - 2.*size;
+    float r4 = length(P-c1)-size/5;
+    return max( min(r1,max(max(r2,r3),-P.y)), -r4);
+}
diff --git a/vispy/glsl/markers/ring.glsl b/vispy/glsl/markers/ring.glsl
new file mode 100644
index 0000000..695cbac
--- /dev/null
+++ b/vispy/glsl/markers/ring.glsl
@@ -0,0 +1,11 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+float marker_ring(vec2 P, float size)
+{
+    float r1 = length(P) - size/2;
+    float r2 = length(P) - size/4;
+    return max(r1,-r2);
+}
diff --git a/vispy/glsl/markers/spade.glsl b/vispy/glsl/markers/spade.glsl
new file mode 100644
index 0000000..23d409e
--- /dev/null
+++ b/vispy/glsl/markers/spade.glsl
@@ -0,0 +1,28 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_spade(vec2 P, float size)
+{
+   // Reversed heart (diamond + 2 circles)
+   float s= size * 0.85 / 3.5;
+   float x = M_SQRT2/2.0 * (P.x + P.y) + 0.4*s;
+   float y = M_SQRT2/2.0 * (P.x - P.y) - 0.4*s;
+   float r1 = max(abs(x),abs(y)) - s;
+   float r2 = length(P - M_SQRT2/2.0*vec2(+1.0,+0.2)*s) - s;
+   float r3 = length(P - M_SQRT2/2.0*vec2(-1.0,+0.2)*s) - s;
+   float r4 =  min(min(r1,r2),r3);
+
+   // Root (2 circles and 2 planes)
+   const vec2 c1 = vec2(+0.65, 0.125);
+   const vec2 c2 = vec2(-0.65, 0.125);
+   float r5 = length(P-c1*size) - size/1.6;
+   float r6 = length(P-c2*size) - size/1.6;
+   float r7 = P.y - 0.5*size;
+   float r8 = 0.1*size - P.y;
+   float r9 = max(-min(r5,r6), max(r7,r8));
+
+    return min(r4,r9);
+}
diff --git a/vispy/glsl/markers/square.glsl b/vispy/glsl/markers/square.glsl
new file mode 100644
index 0000000..e0710a4
--- /dev/null
+++ b/vispy/glsl/markers/square.glsl
@@ -0,0 +1,10 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_square(vec2 P, float size)
+{
+    return max(abs(P.x), abs(P.y)) - size/2.0;
+}
diff --git a/vispy/glsl/markers/tag.glsl b/vispy/glsl/markers/tag.glsl
new file mode 100644
index 0000000..56fa2fc
--- /dev/null
+++ b/vispy/glsl/markers/tag.glsl
@@ -0,0 +1,11 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+float marker_tag(vec2 P, float size)
+{
+    float r1 = max(abs(P.x)- size/2.0, abs(P.y)- size/6.0);
+    float r2 = abs(P.x-size/2.0)+abs(P.y)-size;
+    return max(r1,.75*r2);
+}
diff --git a/vispy/glsl/markers/triangle.glsl b/vispy/glsl/markers/triangle.glsl
new file mode 100644
index 0000000..6eb8dfa
--- /dev/null
+++ b/vispy/glsl/markers/triangle.glsl
@@ -0,0 +1,14 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+float marker_triangle(vec2 P, float size)
+{
+    float x = M_SQRT2/2.0 * (P.x - (P.y-size/6));
+    float y = M_SQRT2/2.0 * (P.x + (P.y-size/6));
+    float r1 = max(abs(x), abs(y)) - size/(2.0*M_SQRT2);
+    float r2 = P.y-size/6;
+    return max(r1,r2);
+}
diff --git a/vispy/glsl/markers/vbar.glsl b/vispy/glsl/markers/vbar.glsl
new file mode 100644
index 0000000..fad30cd
--- /dev/null
+++ b/vispy/glsl/markers/vbar.glsl
@@ -0,0 +1,9 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// -----------------------------------------------------------------------------
+
+float marker_vbar(vec2 P, float size)
+{
+    return max(abs(P.y)- size/2.0, abs(P.x)- size/6.0);
+}
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/glsl/math/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/glsl/math/__init__.py
diff --git a/vispy/glsl/math/circle-through-2-points.glsl b/vispy/glsl/math/circle-through-2-points.glsl
new file mode 100644
index 0000000..474b245
--- /dev/null
+++ b/vispy/glsl/math/circle-through-2-points.glsl
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+
+   Computes the center of the 2 circles with given radius passing through
+   p1 & p2
+
+   Parameters:
+   -----------
+
+   p0, p1: Points ascribed in the circles
+   radius: Radius of the circle
+
+   Return:
+   -------
+   Centers of the two circles with specified radius
+
+   --------------------------------------------------------- */
+
+vec4 circle_from_2_points(vec2 p1, vec2 p2, float radius)
+{
+    float q = length(p2-p1);
+    vec2 m = (p1+p2)/2.0;
+    vec2 d = vec2( sqrt(radius*radius - (q*q/4.0)) * (p1.y-p2.y)/q,
+                   sqrt(radius*radius - (q*q/4.0)) * (p2.x-p1.x)/q);
+    return  vec4(m+d, m-d);
+}
diff --git a/vispy/glsl/math/constants.glsl b/vispy/glsl/math/constants.glsl
new file mode 100644
index 0000000..b72d62b
--- /dev/null
+++ b/vispy/glsl/math/constants.glsl
@@ -0,0 +1,48 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+#ifndef _CONSTANTS_
+#define _CONSTANTS_
+
+// The base of natural logarithms (e)
+const float M_E = 2.71828182845904523536028747135266250;
+
+// The logarithm to base 2 of M_E (log2(e))
+const float M_LOG2E = 1.44269504088896340735992468100189214;
+
+// The logarithm to base 10 of M_E (log10(e))
+const float M_LOG10E = 0.434294481903251827651128918916605082;
+
+// The natural logarithm of 2 (loge(2))
+const float M_LN2 = 0.693147180559945309417232121458176568;
+
+// The natural logarithm of 10 (loge(10))
+const float M_LN10 = 2.30258509299404568401799145468436421;
+
+// Pi, the ratio of a circle's circumference to its diameter.
+const float M_PI = 3.14159265358979323846264338327950288;
+
+// Pi divided by two (pi/2)
+const float M_PI_2 = 1.57079632679489661923132169163975144;
+
+// Pi divided by four  (pi/4)
+const float M_PI_4 = 0.785398163397448309615660845819875721;
+
+// The reciprocal of pi (1/pi)
+const float M_1_PI = 0.318309886183790671537767526745028724;
+
+// Two times the reciprocal of pi (2/pi)
+const float M_2_PI = 0.636619772367581343075535053490057448;
+
+// Two times the reciprocal of the square root of pi (2/sqrt(pi))
+const float M_2_SQRTPI = 1.12837916709551257389615890312154517;
+
+// The square root of two (sqrt(2))
+const float M_SQRT2 = 1.41421356237309504880168872420969808;
+
+// The reciprocal of the square root of two (1/sqrt(2))
+const float M_SQRT1_2 = 0.707106781186547524400844362104849039;
+
+#endif
diff --git a/vispy/glsl/math/double.glsl b/vispy/glsl/math/double.glsl
new file mode 100644
index 0000000..855d5d3
--- /dev/null
+++ b/vispy/glsl/math/double.glsl
@@ -0,0 +1,114 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+/*
+  This shader program emulates double-precision variables using a vec2 instead
+  of single-precision floats. Any function starting with double_* operates on
+  these variables. See http://www.thasler.org/blog/?p=93.
+
+  NOTE: Some NVIDIA cards optimize the double-precision code away. Results are
+  therefore hardware dependent.
+*/
+#define double vec2
+
+
+/* -------------------------------------------------------------------------
+
+   Create an emulated double by storing first part of float in first half of
+   vec2
+
+   ------------------------------------------------------------------------- */
+
+vec2 double_set(float value)
+{
+    double result;
+    result.x = value;
+    result.y = 0.0;
+    return result;
+}
+
+
+
+/* -------------------------------------------------------------------------
+
+   Add two emulated doubles. Complexity comes from carry-over.
+
+   ------------------------------------------------------------------------- */
+
+vec2 double_add(double value_a, double value_b)
+{
+    double result;
+    float t1, t2, e;
+
+    t1 = value_a.x + value_b.x;
+    e = t1 - value_a.x;
+    t2 = ((value_b.x - e) + (value_a.x - (t1 - e))) + value_a.y + value_b.y;
+    result.x = t1 + t2;
+    result.y = t2 - (result.x - t1);
+    return dsc;
+}
+
+
+
+/* -------------------------------------------------------------------------
+
+   Multiply two emulated doubles.
+
+   ------------------------------------------------------------------------- */
+
+vec2 double_mul(double value_a, double value_b)
+{
+    double result;
+    float c11, c21, c2, e, t1, t2;
+    float a1, a2, b1, b2, cona, conb, split = 8193.;
+
+    cona = value_a.x * split;
+    conb = value_b.x * split;
+    a1 = cona - (cona - value_a.x);
+    b1 = conb - (conb - value_b.x);
+    a2 = value_a.x - a1;
+    b2 = value_b.x - b1;
+
+    c11 = value_a.x * value_b.x;
+    c21 = a2 * b2 + (a2 * b1 + (a1 * b2 + (a1 * b1 - c11)));
+
+    c2 = value_a.x * value_b.y + value_a.y * value_b.x;
+
+    t1 = c11 + c2;
+    e = t1 - c11;
+    t2 = value_a.y * value_b.y + ((c2 - e) + (c11 - (t1 - e))) + c21;
+
+    result.x = t1 + t2;
+    result.y = t2 - (result.x - t1);
+
+    return result;
+}
+
+
+
+/* -------------------------------------------------------------------------
+
+   Compare two emulated doubles.
+   Return -1 if a < b
+           0 if a == b
+           1 if a > b
+
+   ------------------------------------------------------------------------- */
+
+float double_compare(double value_a, double value_b)
+{
+    if (value_a.x < value_b.x) {
+        return -1.;
+    } else if (value_a.x == value_b.x) {
+        if (value_a.y < value_b.y) {
+            return -1.;
+        } else if (value_a.y == value_b.y) {
+            return 0.;
+        } else {
+            return 1.;
+        }
+    } else {
+        return 1.;
+    }
+}
diff --git a/vispy/glsl/math/functions.glsl b/vispy/glsl/math/functions.glsl
new file mode 100644
index 0000000..7a0b511
--- /dev/null
+++ b/vispy/glsl/math/functions.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+   Hyperbolic cosine
+   --------------------------------------------------------- */
+float cosh(float x)
+{
+    return 0.5 * (exp(x)+exp(-x));
+}
+
+/* ---------------------------------------------------------
+   Hyperbolic sine
+   --------------------------------------------------------- */
+float sinh(float x)
+{
+    return 0.5 * (exp(x)-exp(-x));
+}
diff --git a/vispy/glsl/math/point-to-line-distance.glsl b/vispy/glsl/math/point-to-line-distance.glsl
new file mode 100644
index 0000000..79b7b72
--- /dev/null
+++ b/vispy/glsl/math/point-to-line-distance.glsl
@@ -0,0 +1,31 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+   Compute distance from a point to a line (2d)
+
+   Parameters:
+   -----------
+
+   p0, p1: Points describing the line
+   p: Point to computed distance to
+
+   Return:
+   -------
+   Distance of p to (p0,p1)
+
+   --------------------------------------------------------- */
+float point_to_line_distance(vec2 p0, vec2 p1, vec2 p)
+{
+    // Projection p' of p such that p' = p0 + u*(p1-p0)
+    vec2 v = p1 - p0;
+    float l2 = v.x*v.x + v.y*v.y;
+    float u = ((p.x-p0.x)*v.x + (p.y-p0.y)*v.y) / l2;
+
+    // h is the projection of p on (p0,p1)
+    vec2 h = p0 + u*v;
+
+    return length(p-h);
+}
diff --git a/vispy/glsl/math/point-to-line-projection.glsl b/vispy/glsl/math/point-to-line-projection.glsl
new file mode 100644
index 0000000..d3de9cb
--- /dev/null
+++ b/vispy/glsl/math/point-to-line-projection.glsl
@@ -0,0 +1,29 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+
+   Project a point p onto a line (p0,p1) and return linear position u such that
+   p' = p0 + u*(p1-p0)
+
+   Parameters:
+   -----------
+
+   p0, p1: Points describing the line
+   p: Point to be projected
+
+   Return:
+   -------
+   Linear position of p onto (p0,p1)
+
+   --------------------------------------------------------- */
+float point_to_line_projection(vec2 p0, vec2 p1, vec2 p)
+{
+    // Projection p' of p such that p' = p0 + u*(p1-p0)
+    // Then  u *= lenght(p1-p0)
+    vec2 v = p1 - p0;
+    float l = length(v);
+    return ((p.x-p0.x)*v.x + (p.y-p0.y)*v.y) / l;
+}
diff --git a/vispy/glsl/math/signed-line-distance.glsl b/vispy/glsl/math/signed-line-distance.glsl
new file mode 100644
index 0000000..b25fcdd
--- /dev/null
+++ b/vispy/glsl/math/signed-line-distance.glsl
@@ -0,0 +1,27 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+
+   Computes the signed distance from a line
+
+   Parameters:
+   -----------
+
+   p0, p1: Points describing the line
+   p: Point to measure distance from
+
+   Return:
+   -------
+   Signed distance
+
+   --------------------------------------------------------- */
+float line_distance(vec2 p, vec2 p1, vec2 p2) {
+    vec2 center = (p1 + p2) * 0.5;
+    float len = length(p2 - p1);
+    vec2 dir = (p2 - p1) / len;
+    vec2 rel_p = p - center;
+    return dot(rel_p, vec2(dir.y, -dir.x));
+}
diff --git a/vispy/glsl/math/signed-segment-distance.glsl b/vispy/glsl/math/signed-segment-distance.glsl
new file mode 100644
index 0000000..a2b58bc
--- /dev/null
+++ b/vispy/glsl/math/signed-segment-distance.glsl
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+
+   Computes the signed distance from a line segment
+
+   Parameters:
+   -----------
+
+   p0, p1: Points describing the line segment
+   p: Point to measure distance from
+
+   Return:
+   -------
+   Signed distance
+
+   --------------------------------------------------------- */
+
+float segment_distance(vec2 p, vec2 p1, vec2 p2) {
+    vec2 center = (p1 + p2) * 0.5;
+    float len = length(p2 - p1);
+    vec2 dir = (p2 - p1) / len;
+    vec2 rel_p = p - center;
+    float dist1 = abs(dot(rel_p, vec2(dir.y, -dir.x)));
+    float dist2 = abs(dot(rel_p, dir)) - 0.5*len;
+    return max(dist1, dist2);
+}
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/glsl/misc/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/glsl/misc/__init__.py
diff --git a/vispy/glsl/misc/regular-grid.frag b/vispy/glsl/misc/regular-grid.frag
new file mode 100644
index 0000000..778a6a6
--- /dev/null
+++ b/vispy/glsl/misc/regular-grid.frag
@@ -0,0 +1,244 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Constants
+// ------------------------------------
+const float M_PI = 3.14159265358979323846;
+const float M_SQRT2 = 1.41421356237309504880;
+
+
+// Uniforms
+// ------------------------------------
+
+// Line antialias area (usually 1 pixel)
+uniform float u_antialias;
+
+// Cartesian limits
+uniform vec4 u_limits1;
+
+// Projected limits
+uniform vec4 u_limits2;
+
+// Major grid steps
+uniform vec2 u_major_grid_step;
+
+// Minor grid steps
+uniform vec2 u_minor_grid_step;
+
+// Major grid line width (1.50 pixel)
+uniform float u_major_grid_width;
+
+// Minor grid line width (0.75 pixel)
+uniform float u_minor_grid_width;
+
+// Major grid line color
+uniform vec4 u_major_grid_color;
+
+// Minor grid line color
+uniform vec4 u_minor_grid_color;
+
+
+// Varyings
+// ------------------------------------
+
+// Texture coordinates (from (-0.5,-0.5) to (+0.5,+0.5)
+varying vec2 v_texcoord;
+
+// Quad size (pixels)
+varying vec2 v_size;
+
+
+
+// Functions
+// ------------------------------------
+
+// Hammer forward transform
+// ------------------------------------
+vec2 transform_forward(vec2 P)
+{
+    const float B = 2.0;
+    float longitude = P.x;
+    float latitude  = P.y;
+    float cos_lat = cos(latitude);
+    float sin_lat = sin(latitude);
+    float cos_lon = cos(longitude/B);
+    float sin_lon = sin(longitude/B);
+    float d = sqrt(1.0 + cos_lat * cos_lon);
+    float x = (B * M_SQRT2 * cos_lat * sin_lon) / d;
+    float y =     (M_SQRT2 * sin_lat) / d;
+    return vec2(x,y);
+}
+
+// Hammer inverse transform
+// ------------------------------------
+vec2 transform_inverse(vec2 P)
+{
+    const float B = 2.0;
+    float x = P.x;
+    float y = P.y;
+    float z = 1.0 - (x*x/16.0) - (y*y/4.0);
+    if (z < 0.0) discard;
+    z = sqrt(z);
+    float lon = 2.0*atan( (z*x),(2.0*(2.0*z*z - 1.0)));
+    float lat = asin(z*y);
+    return vec2(lon,lat);
+}
+
+/*
+// Forward transform (polar)
+// ------------------------------------
+vec2 transform_forward(vec2 P)
+{
+    float x = P.x * cos(P.y);
+    float y = P.x * sin(P.y);
+    return vec2(x,y);
+}
+
+// Inverse transform (polar)
+// ------------------------------------
+vec2 transform_inverse(vec2 P)
+{
+    float rho = length(P);
+    float theta = atan(P.y,P.x);
+    if( theta < 0.0 )
+        theta = 2.0*M_PI+theta;
+    return vec2(rho,theta);
+}
+*/
+
+// [-0.5,-0.5]x[0.5,0.5] -> [xmin,xmax]x[ymin,ymax]
+// ------------------------------------------------
+vec2 scale_forward(vec2 P, vec4 limits)
+{
+    // limits = xmin,xmax,ymin,ymax
+    P += vec2(.5,.5);
+    P *= vec2(limits[1] - limits[0], limits[3]-limits[2]);
+    P += vec2(limits[0], limits[2]);
+    return P;
+}
+
+// [xmin,xmax]x[ymin,ymax] -> [-0.5,-0.5]x[0.5,0.5]
+// ------------------------------------------------
+vec2 scale_inverse(vec2 P, vec4 limits)
+{
+    // limits = xmin,xmax,ymin,ymax
+    P -= vec2(limits[0], limits[2]);
+    P /= vec2(limits[1]-limits[0], limits[3]-limits[2]);
+    return P - vec2(.5,.5);
+}
+
+// Antialias stroke alpha coeff
+float stroke_alpha(float distance, float linewidth, float antialias)
+{
+    float t = linewidth/2.0 - antialias;
+    float signed_distance = distance;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/antialias;
+    alpha = exp(-alpha*alpha);
+
+    if( border_distance > (linewidth/2.0 + antialias) )
+        return 0.0;
+    else if( border_distance < 0.0 )
+        return 1.0;
+    else
+        return alpha;
+}
+
+// Compute the nearest tick from a (normalized) t value
+float get_tick(float t, float vmin, float vmax, float step)
+{
+    float first_tick = floor((vmin + step/2.0)/step) * step;
+    float last_tick = floor((vmax - step/2.0)/step) * step;
+    float tick = vmin + t*(vmax-vmin);
+    if (tick < (vmin + (first_tick-vmin)/2.0))
+        return vmin;
+    if (tick > (last_tick + (vmax-last_tick)/2.0))
+        return vmax;
+    tick += step/2.0;
+    tick = floor(tick/step)*step;
+    return min(max(vmin,tick),vmax);
+}
+
+
+void main()
+{
+    vec2 NP1 = v_texcoord;
+    vec2 P1 = scale_forward(NP1, u_limits1);
+    vec2 P2 = transform_inverse(P1);
+
+    // Test if we are within limits but we do not discard yet because we want
+    // to draw border. Discarding would mean half of the exterior not drawn.
+    bvec2 outside = bvec2(false);
+    if( P2.x < u_limits2[0] ) outside.x = true;
+    if( P2.x > u_limits2[1] ) outside.x = true;
+    if( P2.y < u_limits2[2] ) outside.y = true;
+    if( P2.y > u_limits2[3] ) outside.y = true;
+
+    vec2 NP2 = scale_inverse(P2,u_limits2);
+    vec2 P;
+    float tick;
+
+    tick = get_tick(NP2.x+.5, u_limits2[0], u_limits2[1], u_major_grid_step[0]);
+    P = transform_forward(vec2(tick,P2.y));
+    P = scale_inverse(P, u_limits1);
+    float Mx = length(v_size * (NP1 - P));
+
+    tick = get_tick(NP2.x+.5, u_limits2[0], u_limits2[1], u_minor_grid_step[0]);
+    P = transform_forward(vec2(tick,P2.y));
+    P = scale_inverse(P, u_limits1);
+    float mx = length(v_size * (NP1 - P));
+
+    tick = get_tick(NP2.y+.5, u_limits2[2], u_limits2[3], u_major_grid_step[1]);
+    P = transform_forward(vec2(P2.x,tick));
+    P = scale_inverse(P, u_limits1);
+    float My = length(v_size * (NP1 - P));
+
+    tick = get_tick(NP2.y+.5, u_limits2[2], u_limits2[3], u_minor_grid_step[1]);
+    P = transform_forward(vec2(P2.x,tick));
+    P = scale_inverse(P, u_limits1);
+    float my = length(v_size * (NP1 - P));
+
+    float M = min(Mx,My);
+    float m = min(mx,my);
+
+    // Here we take care of "finishing" the border lines
+    if( outside.x && outside.y ) {
+        if (Mx > 0.5*(u_major_grid_width + u_antialias)) {
+            discard;
+        } else if (My > 0.5*(u_major_grid_width + u_antialias)) {
+            discard;
+        } else {
+            M = max(Mx,My);
+        }
+    } else if( outside.x ) {
+        if (Mx > 0.5*(u_major_grid_width + u_antialias)) {
+            discard;
+        } else {
+            M = m = Mx;
+        }
+    } else if( outside.y ) {
+        if (My > 0.5*(u_major_grid_width + u_antialias)) {
+            discard;
+        } else {
+            M = m = My;
+        }
+    }
+
+    // Mix major/minor colors to get dominant color
+    vec4 color = u_major_grid_color;
+    float alpha1 = stroke_alpha( M, u_major_grid_width, u_antialias);
+    float alpha2 = stroke_alpha( m, u_minor_grid_width, u_antialias);
+    float alpha  = alpha1;
+    if( alpha2 > alpha1*1.5 )
+    {
+        alpha = alpha2;
+        color = u_minor_grid_color;
+    }
+
+    // For the same price you could project a texture
+    // vec4 texcolor = texture2D(u_texture, vec2(NP2.x, 1.0-NP2.y));
+    // gl_FragColor = mix(texcolor, color, alpha);
+    gl_FragColor = vec4(color.rgb, color.a*alpha);
+}
diff --git a/vispy/glsl/misc/spatial-filters.frag b/vispy/glsl/misc/spatial-filters.frag
new file mode 100644
index 0000000..c989769
--- /dev/null
+++ b/vispy/glsl/misc/spatial-filters.frag
@@ -0,0 +1,322 @@
+// ------------------------------------
+// Automatically generated, do not edit
+// ------------------------------------
+
+const float kernel_bias  = -0.234377;
+const float kernel_scale = 1.241974;
+uniform sampler2D u_kernel;
+
+vec4
+filter1D_radius1( sampler2D kernel, float index, float x, vec4 c0, vec4 c1 )
+{
+    float w, w_sum = 0.0;
+    vec4 r = vec4(0.0,0.0,0.0,0.0);
+    w = texture2D(kernel, vec2(0.000000+(x/1.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c0 * w;
+    w = texture2D(kernel, vec2(1.000000-(x/1.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c1 * w;
+    return r;
+}
+vec4
+filter2D_radius1(sampler2D texture, sampler2D kernel, float index, vec2 uv, vec2 pixel )
+{
+    vec2 texel = uv/pixel - vec2(0.0,0.0) ;
+    vec2 f = fract(texel);
+    texel = (texel-fract(texel)+vec2(0.001,0.001))*pixel;
+    vec4 t0 = filter1D_radius1(kernel, index, f.x,
+        texture2D( texture, texel + vec2(0,0)*pixel),
+        texture2D( texture, texel + vec2(1,0)*pixel));
+    vec4 t1 = filter1D_radius1(kernel, index, f.x,
+        texture2D( texture, texel + vec2(0,1)*pixel),
+        texture2D( texture, texel + vec2(1,1)*pixel));
+    return filter1D_radius1(kernel, index, f.y, t0, t1);
+}
+
+vec4
+filter1D_radius2( sampler2D kernel, float index, float x, vec4 c0, vec4 c1, vec4 c2, vec4 c3 )
+{
+    float w, w_sum = 0.0;
+    vec4 r = vec4(0.0,0.0,0.0,0.0);
+    w = texture2D(kernel, vec2(0.500000+(x/2.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c0 * w;
+    w = texture2D(kernel, vec2(0.500000-(x/2.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c2 * w;
+    w = texture2D(kernel, vec2(0.000000+(x/2.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c1 * w;
+    w = texture2D(kernel, vec2(1.000000-(x/2.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c3 * w;
+    return r;
+}
+vec4
+filter2D_radius2(sampler2D texture, sampler2D kernel, float index, vec2 uv, vec2 pixel )
+{
+    vec2 texel = uv/pixel - vec2(0.0,0.0) ;
+    vec2 f = fract(texel);
+    texel = (texel-fract(texel)+vec2(0.001,0.001))*pixel;
+    vec4 t0 = filter1D_radius2(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-1,-1)*pixel),
+        texture2D( texture, texel + vec2(0,-1)*pixel),
+        texture2D( texture, texel + vec2(1,-1)*pixel),
+        texture2D( texture, texel + vec2(2,-1)*pixel));
+    vec4 t1 = filter1D_radius2(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-1,0)*pixel),
+        texture2D( texture, texel + vec2(0,0)*pixel),
+        texture2D( texture, texel + vec2(1,0)*pixel),
+        texture2D( texture, texel + vec2(2,0)*pixel));
+    vec4 t2 = filter1D_radius2(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-1,1)*pixel),
+        texture2D( texture, texel + vec2(0,1)*pixel),
+        texture2D( texture, texel + vec2(1,1)*pixel),
+        texture2D( texture, texel + vec2(2,1)*pixel));
+    vec4 t3 = filter1D_radius2(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-1,2)*pixel),
+        texture2D( texture, texel + vec2(0,2)*pixel),
+        texture2D( texture, texel + vec2(1,2)*pixel),
+        texture2D( texture, texel + vec2(2,2)*pixel));
+    return filter1D_radius2(kernel, index, f.y, t0, t1, t2, t3);
+}
+
+vec4
+filter1D_radius3( sampler2D kernel, float index, float x, vec4 c0, vec4 c1, vec4 c2, vec4 c3, vec4 c4, vec4 c5 )
+{
+    float w, w_sum = 0.0;
+    vec4 r = vec4(0.0,0.0,0.0,0.0);
+    w = texture2D(kernel, vec2(0.666667+(x/3.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c0 * w;
+    w = texture2D(kernel, vec2(0.333333-(x/3.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c3 * w;
+    w = texture2D(kernel, vec2(0.333333+(x/3.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c1 * w;
+    w = texture2D(kernel, vec2(0.666667-(x/3.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c4 * w;
+    w = texture2D(kernel, vec2(0.000000+(x/3.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c2 * w;
+    w = texture2D(kernel, vec2(1.000000-(x/3.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c5 * w;
+    return r;
+}
+vec4
+filter2D_radius3(sampler2D texture, sampler2D kernel, float index, vec2 uv, vec2 pixel )
+{
+    vec2 texel = uv/pixel - vec2(0.0,0.0) ;
+    vec2 f = fract(texel);
+    texel = (texel-fract(texel)+vec2(0.001,0.001))*pixel;
+    vec4 t0 = filter1D_radius3(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-2,-2)*pixel),
+        texture2D( texture, texel + vec2(-1,-2)*pixel),
+        texture2D( texture, texel + vec2(0,-2)*pixel),
+        texture2D( texture, texel + vec2(1,-2)*pixel),
+        texture2D( texture, texel + vec2(2,-2)*pixel),
+        texture2D( texture, texel + vec2(3,-2)*pixel));
+    vec4 t1 = filter1D_radius3(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-2,-1)*pixel),
+        texture2D( texture, texel + vec2(-1,-1)*pixel),
+        texture2D( texture, texel + vec2(0,-1)*pixel),
+        texture2D( texture, texel + vec2(1,-1)*pixel),
+        texture2D( texture, texel + vec2(2,-1)*pixel),
+        texture2D( texture, texel + vec2(3,-1)*pixel));
+    vec4 t2 = filter1D_radius3(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-2,0)*pixel),
+        texture2D( texture, texel + vec2(-1,0)*pixel),
+        texture2D( texture, texel + vec2(0,0)*pixel),
+        texture2D( texture, texel + vec2(1,0)*pixel),
+        texture2D( texture, texel + vec2(2,0)*pixel),
+        texture2D( texture, texel + vec2(3,0)*pixel));
+    vec4 t3 = filter1D_radius3(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-2,1)*pixel),
+        texture2D( texture, texel + vec2(-1,1)*pixel),
+        texture2D( texture, texel + vec2(0,1)*pixel),
+        texture2D( texture, texel + vec2(1,1)*pixel),
+        texture2D( texture, texel + vec2(2,1)*pixel),
+        texture2D( texture, texel + vec2(3,1)*pixel));
+    vec4 t4 = filter1D_radius3(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-2,2)*pixel),
+        texture2D( texture, texel + vec2(-1,2)*pixel),
+        texture2D( texture, texel + vec2(0,2)*pixel),
+        texture2D( texture, texel + vec2(1,2)*pixel),
+        texture2D( texture, texel + vec2(2,2)*pixel),
+        texture2D( texture, texel + vec2(3,2)*pixel));
+    vec4 t5 = filter1D_radius3(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-2,3)*pixel),
+        texture2D( texture, texel + vec2(-1,3)*pixel),
+        texture2D( texture, texel + vec2(0,3)*pixel),
+        texture2D( texture, texel + vec2(1,3)*pixel),
+        texture2D( texture, texel + vec2(2,3)*pixel),
+        texture2D( texture, texel + vec2(3,3)*pixel));
+    return filter1D_radius3(kernel, index, f.y, t0, t1, t2, t3, t4, t5);
+}
+
+vec4
+filter1D_radius4( sampler2D kernel, float index, float x, vec4 c0, vec4 c1, vec4 c2, vec4 c3, vec4 c4, vec4 c5, vec4 c6, vec4 c7 )
+{
+    float w, w_sum = 0.0;
+    vec4 r = vec4(0.0,0.0,0.0,0.0);
+    w = texture2D(kernel, vec2(0.750000+(x/4.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c0 * w;
+    w = texture2D(kernel, vec2(0.250000-(x/4.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c4 * w;
+    w = texture2D(kernel, vec2(0.500000+(x/4.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c1 * w;
+    w = texture2D(kernel, vec2(0.500000-(x/4.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c5 * w;
+    w = texture2D(kernel, vec2(0.250000+(x/4.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c2 * w;
+    w = texture2D(kernel, vec2(0.750000-(x/4.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c6 * w;
+    w = texture2D(kernel, vec2(0.000000+(x/4.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c3 * w;
+    w = texture2D(kernel, vec2(1.000000-(x/4.0),index) ).r;
+    w = w*kernel_scale + kernel_bias;
+    r += c7 * w;
+    return r;
+}
+vec4
+filter2D_radius4(sampler2D texture, sampler2D kernel, float index, vec2 uv, vec2 pixel )
+{
+    vec2 texel = uv/pixel - vec2(0.0,0.0) ;
+    vec2 f = fract(texel);
+    texel = (texel-fract(texel)+vec2(0.001,0.001))*pixel;
+    vec4 t0 = filter1D_radius4(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-3,-3)*pixel),
+        texture2D( texture, texel + vec2(-2,-3)*pixel),
+        texture2D( texture, texel + vec2(-1,-3)*pixel),
+        texture2D( texture, texel + vec2(0,-3)*pixel),
+        texture2D( texture, texel + vec2(1,-3)*pixel),
+        texture2D( texture, texel + vec2(2,-3)*pixel),
+        texture2D( texture, texel + vec2(3,-3)*pixel),
+        texture2D( texture, texel + vec2(4,-3)*pixel));
+    vec4 t1 = filter1D_radius4(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-3,-2)*pixel),
+        texture2D( texture, texel + vec2(-2,-2)*pixel),
+        texture2D( texture, texel + vec2(-1,-2)*pixel),
+        texture2D( texture, texel + vec2(0,-2)*pixel),
+        texture2D( texture, texel + vec2(1,-2)*pixel),
+        texture2D( texture, texel + vec2(2,-2)*pixel),
+        texture2D( texture, texel + vec2(3,-2)*pixel),
+        texture2D( texture, texel + vec2(4,-2)*pixel));
+    vec4 t2 = filter1D_radius4(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-3,-1)*pixel),
+        texture2D( texture, texel + vec2(-2,-1)*pixel),
+        texture2D( texture, texel + vec2(-1,-1)*pixel),
+        texture2D( texture, texel + vec2(0,-1)*pixel),
+        texture2D( texture, texel + vec2(1,-1)*pixel),
+        texture2D( texture, texel + vec2(2,-1)*pixel),
+        texture2D( texture, texel + vec2(3,-1)*pixel),
+        texture2D( texture, texel + vec2(4,-1)*pixel));
+    vec4 t3 = filter1D_radius4(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-3,0)*pixel),
+        texture2D( texture, texel + vec2(-2,0)*pixel),
+        texture2D( texture, texel + vec2(-1,0)*pixel),
+        texture2D( texture, texel + vec2(0,0)*pixel),
+        texture2D( texture, texel + vec2(1,0)*pixel),
+        texture2D( texture, texel + vec2(2,0)*pixel),
+        texture2D( texture, texel + vec2(3,0)*pixel),
+        texture2D( texture, texel + vec2(4,0)*pixel));
+    vec4 t4 = filter1D_radius4(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-3,1)*pixel),
+        texture2D( texture, texel + vec2(-2,1)*pixel),
+        texture2D( texture, texel + vec2(-1,1)*pixel),
+        texture2D( texture, texel + vec2(0,1)*pixel),
+        texture2D( texture, texel + vec2(1,1)*pixel),
+        texture2D( texture, texel + vec2(2,1)*pixel),
+        texture2D( texture, texel + vec2(3,1)*pixel),
+        texture2D( texture, texel + vec2(4,1)*pixel));
+    vec4 t5 = filter1D_radius4(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-3,2)*pixel),
+        texture2D( texture, texel + vec2(-2,2)*pixel),
+        texture2D( texture, texel + vec2(-1,2)*pixel),
+        texture2D( texture, texel + vec2(0,2)*pixel),
+        texture2D( texture, texel + vec2(1,2)*pixel),
+        texture2D( texture, texel + vec2(2,2)*pixel),
+        texture2D( texture, texel + vec2(3,2)*pixel),
+        texture2D( texture, texel + vec2(4,2)*pixel));
+    vec4 t6 = filter1D_radius4(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-3,3)*pixel),
+        texture2D( texture, texel + vec2(-2,3)*pixel),
+        texture2D( texture, texel + vec2(-1,3)*pixel),
+        texture2D( texture, texel + vec2(0,3)*pixel),
+        texture2D( texture, texel + vec2(1,3)*pixel),
+        texture2D( texture, texel + vec2(2,3)*pixel),
+        texture2D( texture, texel + vec2(3,3)*pixel),
+        texture2D( texture, texel + vec2(4,3)*pixel));
+    vec4 t7 = filter1D_radius4(kernel, index, f.x,
+        texture2D( texture, texel + vec2(-3,4)*pixel),
+        texture2D( texture, texel + vec2(-2,4)*pixel),
+        texture2D( texture, texel + vec2(-1,4)*pixel),
+        texture2D( texture, texel + vec2(0,4)*pixel),
+        texture2D( texture, texel + vec2(1,4)*pixel),
+        texture2D( texture, texel + vec2(2,4)*pixel),
+        texture2D( texture, texel + vec2(3,4)*pixel),
+        texture2D( texture, texel + vec2(4,4)*pixel));
+    return filter1D_radius4(kernel, index, f.y, t0, t1, t2, t3, t4, t5, t6, t7);
+}
+
+vec4 Nearest(sampler2D texture, vec2 shape, vec2 uv)
+{ return texture2D(texture,uv); }
+
+vec4 Bilinear(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius1(texture, u_kernel, 0.031250, uv, 1.0/shape); }
+
+vec4 Hanning(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius1(texture, u_kernel, 0.093750, uv, 1.0/shape); }
+
+vec4 Hamming(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius1(texture, u_kernel, 0.156250, uv, 1.0/shape); }
+
+vec4 Hermite(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius1(texture, u_kernel, 0.218750, uv, 1.0/shape); }
+
+vec4 Kaiser(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius1(texture, u_kernel, 0.281250, uv, 1.0/shape); }
+
+vec4 Quadric(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius2(texture, u_kernel, 0.343750, uv, 1.0/shape); }
+
+vec4 Bicubic(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius2(texture, u_kernel, 0.406250, uv, 1.0/shape); }
+
+vec4 CatRom(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius2(texture, u_kernel, 0.468750, uv, 1.0/shape); }
+
+vec4 Mitchell(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius2(texture, u_kernel, 0.531250, uv, 1.0/shape); }
+
+vec4 Spline16(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius2(texture, u_kernel, 0.593750, uv, 1.0/shape); }
+
+vec4 Spline36(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius3(texture, u_kernel, 0.656250, uv, 1.0/shape); }
+
+vec4 Gaussian(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius2(texture, u_kernel, 0.718750, uv, 1.0/shape); }
+
+vec4 Bessel(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius4(texture, u_kernel, 0.781250, uv, 1.0/shape); }
+
+vec4 Sinc(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius4(texture, u_kernel, 0.843750, uv, 1.0/shape); }
+
+vec4 Lanczos(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius4(texture, u_kernel, 0.906250, uv, 1.0/shape); }
+
+vec4 Blackman(sampler2D texture, vec2 shape, vec2 uv)
+{ return filter2D_radius4(texture, u_kernel, 0.968750, uv, 1.0/shape); }
diff --git a/vispy/glsl/misc/viewport-NDC.glsl b/vispy/glsl/misc/viewport-NDC.glsl
new file mode 100644
index 0000000..f3b2ed9
--- /dev/null
+++ b/vispy/glsl/misc/viewport-NDC.glsl
@@ -0,0 +1,20 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+vec2 NDC_to_viewport(vec4 position, vec2 viewport)
+{
+    vec2 p = position.xy/position.w;
+    return (p+1.0)/2.0 * viewport;
+}
+
+vec4 viewport_to_NDC(vec2 position, vec2 viewport)
+{
+    return vec4(2.0*(position/viewport) - 1.0, 0.0, 1.0);
+}
+
+vec4 viewport_to_NDC(vec3 position, vec2 viewport)
+{
+    return vec4(2.0*(position.xy/viewport) - 1.0, position.z, 1.0);
+}
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/glsl/transforms/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/glsl/transforms/__init__.py
diff --git a/vispy/glsl/transforms/azimuthal-equal-area.glsl b/vispy/glsl/transforms/azimuthal-equal-area.glsl
new file mode 100644
index 0000000..c7ef5f4
--- /dev/null
+++ b/vispy/glsl/transforms/azimuthal-equal-area.glsl
@@ -0,0 +1,32 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+float scale(float x) { return sqrt(2.0/(1.0+x)); }
+float angle(float x) { return 2.0 * asin(x/2.0); }
+
+vec2 forward(float longitude, float latitude)
+{
+    float cos_lon = cos(longitude);
+    float cos_lat = cos(latitude);
+    float k = scale(cos_lon * cos_lat);
+    return vec2( k * cos_lat * sin(longitude), k * sin(latitude));
+}
+vec2 forward(vec2 P) { return forward(P.x,P.y); }
+vec3 forward(vec3 P) { return vec3(forward(P.x,P.y), P.z); }
+vec4 forward(vec4 P) { return vec4(forward(P.x,P.y), P.z, P.w); }
+
+vec2 inverse(float x, float y)
+{
+    float rho = sqrt(x*x + y*y);
+    float c = angle(rho);
+    float sinc = sin(c);
+    float cosc = cos(c);
+    if (rho != 0)
+        return vec2( atan(x*sinc, rho*cosc), asin(y*sinc/rho));
+    return vec2( atan(x*sinc, rho*cosc), asin(0));
+}
+vec2 inverse(vec2 P) { return inverse(P.x,P.y); }
+vec3 inverse(vec3 P) { return vec3(inverse(P.x,P.y), P.z); }
+vec4 inverse(vec4 P) { return vec4(inverse(P.x,P.y), P.z, P.w); }
diff --git a/vispy/glsl/transforms/azimuthal-equidistant.glsl b/vispy/glsl/transforms/azimuthal-equidistant.glsl
new file mode 100644
index 0000000..c3b2a77
--- /dev/null
+++ b/vispy/glsl/transforms/azimuthal-equidistant.glsl
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+float scale(float x) {
+    float c = acos(x);
+    if (c != 0.0)
+        return c / sin(c);
+    discard;
+ }
+float angle(float x) { return x; }
+
+vec2 forward(float longitude, float latitude)
+{
+    float cos_lon = cos(longitude);
+    float cos_lat = cos(latitude);
+    float k = scale(cos_lon * cos_lat);
+    return vec2( k * cos_lat * sin(longitude), k * sin(latitude));
+}
+vec2 forward(vec2 P) { return forward(P.x,P.y); }
+vec3 forward(vec3 P) { return vec3(forward(P.x,P.y), P.z); }
+vec4 forward(vec4 P) { return vec4(forward(P.x,P.y), P.z, P.w); }
+
+vec2 inverse(float x, float y)
+{
+    float rho = sqrt(x*x + y*y);
+    float c = angle(rho);
+    float sinc = sin(c);
+    float cosc = cos(c);
+    //if (rho != 0)
+    return vec2( atan(x*sinc, rho*cosc), asin(y*sinc/rho));
+    //else
+    //return vec2( atan(x*sinc, rho*cosc), asin(1e10));
+}
+vec2 inverse(vec2 P) { return inverse(P.x,P.y); }
+vec3 inverse(vec3 P) { return vec3(inverse(P.x,P.y), P.z); }
+vec4 inverse(vec4 P) { return vec4(inverse(P.x,P.y), P.z, P.w); }
diff --git a/vispy/glsl/transforms/hammer.glsl b/vispy/glsl/transforms/hammer.glsl
new file mode 100644
index 0000000..0a0efd5
--- /dev/null
+++ b/vispy/glsl/transforms/hammer.glsl
@@ -0,0 +1,44 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+//
+//   Hammer projection
+//   See http://en.wikipedia.org/wiki/Hammer_projection
+//
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+const float B = 2.0;
+
+vec4 forward(float longitude, float latitude, float z, float w)
+{
+    float cos_lat = cos(latitude);
+    float sin_lat = sin(latitude);
+    float cos_lon = cos(longitude/B);
+    float sin_lon = sin(longitude/B);
+    float d = sqrt(1.0 + cos_lat * cos_lon);
+    float x = (B * M_SQRT2 * cos_lat * sin_lon) / d;
+    float y =     (M_SQRT2 * sin_lat) / d;
+    return vec4(x,y,z,w);
+}
+vec4 forward(float x, float y) {return forward(x, y, 0.0, 1.0);}
+vec4 forward(float x, float y, float z) {return forward(x, y, 0.0, 1.0);}
+vec4 forward(vec2 P) { return forward(P.x, P.y); }
+vec4 forward(vec3 P) { return forward(P.x, P.y, P.z, 1.0); }
+vec4 forward(vec4 P) { return forward(P.x, P.y, P.z, P.w); }
+
+
+vec2 inverse(float x, float y)
+{
+    float z = 1.0 - (x*x/16.0) - (y*y/4.0);
+    // if (z < 0.0)
+    //     discard;
+    z = sqrt(z);
+    float longitude = 2.0*atan( (z*x),(2.0*(2.0*z*z - 1.0)));
+    float latitude = asin(z*y);
+    return vec2(longitude, latitude);
+}
+vec2 inverse(vec2 P) { return inverse(P.x,P.y); }
+vec3 inverse(vec3 P) { return vec3(inverse(P.x,P.y), P.z); }
+vec4 inverse(vec4 P) { return vec4(inverse(P.x,P.y), P.z, P.w); }
diff --git a/vispy/glsl/transforms/identity.glsl b/vispy/glsl/transforms/identity.glsl
new file mode 100644
index 0000000..b134989
--- /dev/null
+++ b/vispy/glsl/transforms/identity.glsl
@@ -0,0 +1,6 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+#include "transforms/identity_forward.glsl"
+#include "transforms/identity_inverse.glsl"
diff --git a/vispy/glsl/transforms/identity_forward.glsl b/vispy/glsl/transforms/identity_forward.glsl
new file mode 100644
index 0000000..12e3abf
--- /dev/null
+++ b/vispy/glsl/transforms/identity_forward.glsl
@@ -0,0 +1,23 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+   Forward identity projection (identity)
+
+   Parameters:
+   -----------
+
+   position : 2d position in cartesian coordinates
+
+   Return:
+   -------
+   2d position in cartesian coordinates
+
+   --------------------------------------------------------- */
+
+vec2 transform_identity_forward(vec2 P)
+{
+    return P;
+}
diff --git a/vispy/glsl/transforms/identity_inverse.glsl b/vispy/glsl/transforms/identity_inverse.glsl
new file mode 100644
index 0000000..a8655d3
--- /dev/null
+++ b/vispy/glsl/transforms/identity_inverse.glsl
@@ -0,0 +1,23 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+/* ---------------------------------------------------------
+   Inverse cartesian projection (identity)
+
+   Parameters:
+   -----------
+
+   position : 2d position in cartesian coordinates
+
+   Return:
+   -------
+
+   2d position in cartesian coordinates
+
+   --------------------------------------------------------- */
+vec2 transform_identity_inverse(vec2 P)
+{
+    return P;
+}
diff --git a/vispy/glsl/transforms/linear-scale.glsl b/vispy/glsl/transforms/linear-scale.glsl
new file mode 100644
index 0000000..082b0fb
--- /dev/null
+++ b/vispy/glsl/transforms/linear-scale.glsl
@@ -0,0 +1,127 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+/*
+  Linear scales are the most common scale, and a good default choice to map a
+  continuous input domain to a continuous output range. The mapping is linear
+  in that the output range value y can be expressed as a linear function of the
+  input domain value x: y = mx + b. The input domain is typically a dimension
+  of the data that you want to visualize, such as the height of students
+  (measured in meters) in a sample population. The output range is typically a
+  dimension of the desired output visualization, such as the height of bars
+  (measured in pixels) in a histogram.
+*/
+uniform int  linear_scale_clamp;
+uniform int  linear_scale_discard;
+uniform vec2 linear_scale_range;
+uniform vec2 linear_scale_domain;
+
+
+float forward(float value)
+{
+    vec2 domain = linear_scale_domain;
+    vec2 range = linear_scale_range;
+    float t = (value - domain.x) /(domain.y - domain.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (linear_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (linear_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    return range.x + t*(range.y - range.x);
+}
+
+vec2 forward(vec2 value)
+{
+    vec2 domain = linear_scale_domain;
+    vec2 range = linear_scale_range;
+    vec2 t = (value - domain.x) /(domain.y - domain.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (linear_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (linear_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    return range.x + t*(range.y - range.x);
+}
+
+vec3 forward(vec3 value)
+{
+    vec2 domain = linear_scale_domain;
+    vec2 range = linear_scale_range;
+    vec3 t = (value - domain.x) /(domain.y - domain.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (linear_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (linear_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    return range.x + t*(range.y - range.x);
+}
+
+float inverse(float value)
+{
+    vec2 domain = linear_scale_domain;
+    vec2 range = linear_scale_range;
+    float t = (value - range.x) / (range.y - range.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (linear_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (linear_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    return domain.x + t*(domain.y - domain.x);
+}
+
+vec2 inverse(vec2 value)
+{
+    vec2 domain = linear_scale_domain;
+    vec2 range = linear_scale_range;
+    vec2 t = (value - range.x) / (range.y - range.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (linear_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (linear_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    return domain.x + t*(domain.y - domain.x);
+}
+
+vec3 inverse(vec3 value)
+{
+    vec2 domain = linear_scale_domain;
+    vec2 range = linear_scale_range;
+    vec3 t = (value - range.x) / (range.y - range.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (linear_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (linear_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    return domain.x + t*(domain.y - domain.x);
+}
diff --git a/vispy/glsl/transforms/log-scale.glsl b/vispy/glsl/transforms/log-scale.glsl
new file mode 100644
index 0000000..23b3b78
--- /dev/null
+++ b/vispy/glsl/transforms/log-scale.glsl
@@ -0,0 +1,126 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+uniform int  log_scale_clamp;
+uniform int  log_scale_discard;
+uniform vec2 log_scale_range;
+uniform vec2 log_scale_domain;
+uniform float log_scale_base;
+
+float forward(float value)
+{
+    vec2 domain = log_scale_domain;
+    vec2 range = log_scale_range;
+    float base = log_scale_base;
+
+    float v = log(value) / log(base);
+    float t = (v - domain.x) /(domain.y - domain.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (log_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (log_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+    return sign(value) * (range.x + t*(range.y - range.x));
+}
+
+vec2 forward(vec2 value)
+{
+    vec2 domain = log_scale_domain;
+    vec2 range = log_scale_range;
+    float base = log_scale_base;
+    vec2 v = log(value) / log(base);
+    vec2 t = (v - domain.x) /(domain.y - domain.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (log_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (log_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+    return sign(value) * (range.x + t*(range.y - range.x));
+}
+
+vec3 forward(vec3 value)
+{
+    vec2 domain = log_scale_domain;
+    vec2 range = log_scale_range;
+    float base = log_scale_base;
+    vec3 v = log(value) / log(base);
+    vec3 t = (v - domain.x) /(domain.y - domain.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (log_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (log_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+    return sign(value) * (range.x + t*(range.y - range.x));
+}
+
+
+float inverse(float value)
+{
+    vec2 domain = log_scale_domain;
+    vec2 range = log_scale_range;
+    float base = log_scale_base;
+    float t = (abs(value) - range.x) / (range.y - range.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (log_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (log_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+    float v = domain.x + t*(domain.y - domain.x);
+    return sign(value) * pow(base, abs(v));
+}
+
+vec2 inverse(vec2 value)
+{
+    vec2 domain = log_scale_domain;
+    vec2 range = log_scale_range;
+    float base = log_scale_base;
+    vec2 t = (abs(value) - range.x) / (range.y - range.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (log_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (log_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+    vec2 v = domain.x + t*(domain.y - domain.x);
+    return sign(value) * pow(vec2(base), abs(v));
+}
+
+vec3 inverse(vec3 value)
+{
+    vec2 domain = log_scale_domain;
+    vec2 range = log_scale_range;
+    float base = log_scale_base;
+    vec3 t = (abs(value) - range.x) / (range.y - range.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (log_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (log_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+    vec3 v = domain.x + t*(domain.y - domain.x);
+    return sign(value) * pow(vec3(base), abs(v));
+}
diff --git a/vispy/glsl/transforms/mercator-transverse-forward.glsl b/vispy/glsl/transforms/mercator-transverse-forward.glsl
new file mode 100644
index 0000000..e1456bd
--- /dev/null
+++ b/vispy/glsl/transforms/mercator-transverse-forward.glsl
@@ -0,0 +1,40 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Constants
+// ---------
+const float k0 = 0.75;
+const float a  = 1.00;
+
+// Helper functions
+// ----------------
+float cosh(float x) { return 0.5 * (exp(x)+exp(-x)); }
+float sinh(float x) { return 0.5 * (exp(x)-exp(-x)); }
+
+
+/* ---------------------------------------------------------
+   Transverse Mercator projection
+   -> http://en.wikipedia.org/wiki/Transverse_Mercator_projection
+
+
+   Parameters:
+   -----------
+
+   position : 2d position in (longitude,latitiude) coordinates
+
+   Return:
+   -------
+   2d position in cartesian coordinates
+
+   --------------------------------------------------------- */
+
+vec2 transform_forward(vec2 P)
+{
+    float lambda = P.x;
+    float phi = P.y;
+    float x = 0.5*k0*log((1.0+sin(lambda)*cos(phi)) / (1.0 - sin(lambda)*cos(phi)));
+    float y = k0*a*atan(tan(phi), cos(lambda));
+    return vec2(x,y);
+}
diff --git a/vispy/glsl/transforms/mercator-transverse-inverse.glsl b/vispy/glsl/transforms/mercator-transverse-inverse.glsl
new file mode 100644
index 0000000..6d25b49
--- /dev/null
+++ b/vispy/glsl/transforms/mercator-transverse-inverse.glsl
@@ -0,0 +1,40 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+// Constants
+// ------------------------------------
+const float k0 = 0.75;
+const float a  = 1.00;
+
+
+// Helper functions
+// ------------------------------------
+float cosh(float x) { return 0.5 * (exp(x)+exp(-x)); }
+float sinh(float x) { return 0.5 * (exp(x)-exp(-x)); }
+
+
+/* ---------------------------------------------------------
+   Inverse Lambert azimuthal equal-area projection
+   -> http://en.wikipedia.org/wiki/Transverse_Mercator_projection
+
+   Parameters:
+   -----------
+
+   position : 2d position in cartesian coordinates
+
+   Return:
+   -------
+   2d position in (longitude,latitiude) coordinates
+
+   --------------------------------------------------------- */
+
+vec2 transform_inverse(vec2 P)
+{
+    float x = P.x;
+    float y = P.y;
+    float lambda = atan(sinh(x/(k0*a)),cos(y/(k0*a)));
+    float phi    = asin(sin(y/(k0*a))/cosh(x/(k0*a)));
+    return vec2(lambda,phi);
+}
diff --git a/vispy/glsl/transforms/panzoom.glsl b/vispy/glsl/transforms/panzoom.glsl
new file mode 100644
index 0000000..91f8f1d
--- /dev/null
+++ b/vispy/glsl/transforms/panzoom.glsl
@@ -0,0 +1,10 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+uniform vec2 panzoom_scale;
+uniform vec2 panzoom_translate;
+vec4 panzoom(vec4 position)
+{
+    return vec4(panzoom_scale*position.xy + panzoom_translate, position.z, 1.0);
+}
diff --git a/vispy/glsl/transforms/polar.glsl b/vispy/glsl/transforms/polar.glsl
new file mode 100644
index 0000000..d62cc7c
--- /dev/null
+++ b/vispy/glsl/transforms/polar.glsl
@@ -0,0 +1,41 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+//
+//   Polar projection
+//   See http://en.wikipedia.org/wiki/Hammer_projection
+//
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+uniform float polar_origin;
+
+vec4 forward(float rho, float theta, float z, float w)
+{
+    return vec4(rho * cos(theta + polar_origin),
+                rho * sin(theta + polar_origin),
+                z, w);
+}
+vec4 forward(float x, float y) {return forward(x, y, 0.0, 1.0);}
+vec4 forward(float x, float y, float z) {return forward(x, y, z, 1.0);}
+vec4 forward(vec2 P) { return forward(P.x, P.y); }
+vec4 forward(vec3 P) { return forward(P.x, P.y, P.z, 1.0); }
+vec4 forward(vec4 P) { return forward(P.x, P.y, P.z, P.w); }
+// vec4 forward(float x, float y, float z) { return vec3(forward(x,y),z); }
+
+vec4 inverse(float x, float y, float z, float w)
+{
+    float rho = length(vec2(x,y));
+    float theta = atan(y,x);
+    if( theta < 0.0 )
+        theta = 2.0*M_PI+theta;
+    return vec4(rho, theta-polar_origin, z, w);
+}
+vec4 inverse(float x, float y) {return inverse(x,y,0.0,1.0); }
+vec4 inverse(float x, float y, float z) {return inverse(x,y,z,1.0); }
+vec4 inverse(vec2 P) { return inverse(P.x, P.y, 0.0, 1.0); }
+vec4 inverse(vec3 P) { return inverse(P.x, P.y, P.z, 1.0); }
+vec4 inverse(vec4 P) { return inverse(P.x, P.y, P.z, P.w); }
+
+//vec3 inverse(float x, float y, float z) { return vec3(inverse(x,y),z); }
diff --git a/vispy/glsl/transforms/position.glsl b/vispy/glsl/transforms/position.glsl
new file mode 100644
index 0000000..83f19f8
--- /dev/null
+++ b/vispy/glsl/transforms/position.glsl
@@ -0,0 +1,44 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+vec4 position(float x)
+{
+    return vec4(x, 0.0, 0.0, 1.0);
+}
+
+vec4 position(float x, float y)
+{
+    return vec4(x, y, 0.0, 1.0);
+}
+
+vec4 position(vec2 xy)
+{
+    return vec4(xy, 0.0, 1.0);
+}
+
+vec4 position(float x, float y, float z)
+{
+    return vec4(x, y, z, 1.0);
+}
+
+vec4 position(vec3 xyz)
+{
+    return vec4(xyz, 1.0);
+}
+
+vec4 position(vec4 xyzw)
+{
+    return xyzw;
+}
+
+vec4 position(vec2 xy, float z)
+{
+    return vec4(xy, z, 1.0);
+}
+
+vec4 position(float x, vec2 yz)
+{
+    return vec4(x, yz, 1.0);
+}
diff --git a/vispy/glsl/transforms/power-scale.glsl b/vispy/glsl/transforms/power-scale.glsl
new file mode 100644
index 0000000..7cbd6a7
--- /dev/null
+++ b/vispy/glsl/transforms/power-scale.glsl
@@ -0,0 +1,139 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+/*
+  Power scales are similar to linear scales, except there's an exponential
+  transform that is applied to the input domain value before the output range
+  value is computed. The mapping to the output range value y can be expressed
+  as a function of the input domain value x: y = mx^k + b, where k is the
+  exponent value. Power scales also support negative values, in which case the
+  input value is multiplied by -1, and the resulting output value is also
+  multiplied by -1.
+*/
+
+uniform int  power_scale_clamp;
+uniform int  power_scale_discard;
+uniform vec2 power_scale_range;
+uniform vec2 power_scale_domain;
+uniform float power_scale_exponent;
+
+float forward(float value)
+{
+    vec2 domain = power_scale_domain;
+    vec2 range = power_scale_range;
+    float exponent = power_scale_exponent;
+    float v = pow(abs(value), exponent);
+    float t = (v - domain.x) /(domain.y - domain.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (power_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (power_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    return sign(value) * (range.x + t*(range.y - range.x));
+}
+
+vec2 forward(vec2 value)
+{
+    vec2 domain = power_scale_domain;
+    vec2 range = power_scale_range;
+    float exponent = power_scale_exponent;
+    vec2 v = pow(abs(value), vec2(exponent));
+    vec2 t = (v - domain.x) /(domain.y - domain.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (power_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (power_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    return sign(value) * (range.x + t*(range.y - range.x));
+}
+
+vec3 forward(vec3 value)
+{
+    vec2 domain = power_scale_domain;
+    vec2 range = power_scale_range;
+    float exponent = power_scale_exponent;
+    vec3 v = pow(abs(value), vec3(exponent));
+    vec3 t = (v - domain.x) /(domain.y - domain.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (power_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (power_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    return sign(value) * (range.x + t*(range.y - range.x));
+}
+
+float inverse(float value)
+{
+    vec2 domain = power_scale_domain;
+    vec2 range = power_scale_range;
+    float exponent = power_scale_exponent;
+    float t = (abs(value) - range.x) / (range.y - range.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (power_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (power_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    float v = domain.x + t*(domain.y - domain.x);
+    return sign(value) * pow(abs(v), 1.0/exponent);
+}
+
+vec2 inverse(vec2 value)
+{
+    vec2 domain = power_scale_domain;
+    vec2 range = power_scale_range;
+    float exponent = power_scale_exponent;
+    vec2 t = (abs(value) - range.x) / (range.y - range.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (power_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (power_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    vec2 v = domain.x + t*(domain.y - domain.x);
+    return sign(value) * pow(abs(v), vec2(1.0/exponent));
+}
+
+vec3 inverse(vec3 value)
+{
+    vec2 domain = power_scale_domain;
+    vec2 range = power_scale_range;
+    float exponent = power_scale_exponent;
+    vec3 t = (abs(value) - range.x) / (range.y - range.x);
+
+#ifdef __FRAGMENT_SHADER__
+    if (power_scale_discard > 0)
+        if (t != clamp(t, 0.0, 1.0))
+            discard;
+#endif
+
+    if (power_scale_clamp > 0)
+        t = clamp(t, 0.0, 1.0);
+
+    vec3 v = domain.x + t*(domain.y - domain.x);
+    return sign(value) * pow(abs(v), vec3(1.0/exponent));
+}
diff --git a/vispy/glsl/transforms/projection.glsl b/vispy/glsl/transforms/projection.glsl
new file mode 100644
index 0000000..3aefd0c
--- /dev/null
+++ b/vispy/glsl/transforms/projection.glsl
@@ -0,0 +1,7 @@
+// Simple matrix projection
+uniform mat4 projection;
+
+vec4 transform(vec4 position)
+{
+    return projection*position;
+}
diff --git a/vispy/glsl/transforms/pvm.glsl b/vispy/glsl/transforms/pvm.glsl
new file mode 100644
index 0000000..e9fe23e
--- /dev/null
+++ b/vispy/glsl/transforms/pvm.glsl
@@ -0,0 +1,13 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+uniform mat4 view;
+uniform mat4 model;
+uniform mat4 projection;
+
+vec4 transform(vec4 position)
+{
+    return projection*view*model*position;
+}
diff --git a/vispy/glsl/transforms/rotate.glsl b/vispy/glsl/transforms/rotate.glsl
new file mode 100644
index 0000000..c28aa84
--- /dev/null
+++ b/vispy/glsl/transforms/rotate.glsl
@@ -0,0 +1,45 @@
+// -----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier
+// Distributed under the (new) BSD License. See LICENSE.txt for more info.
+// -----------------------------------------------------------------------------
+uniform vec3 rotate_axis;
+uniform vec3 rotate_origin;
+uniform float rotate_angle;
+uniform mat4 rotate_forward_matrix;
+uniform mat4 rotate_inverse_matrix;
+
+vec2 forward(vec2 position)
+{
+    vec4 P = vec4(position,0.0,1.0);
+    P.xy -= rotate_origin.xy;
+    P = rotate_forward_matrix*P;
+    P.xy += rotate_origin.xy;
+    return P.xy;
+}
+
+vec3 forward(vec3 position)
+{
+    vec4 P = vec4(position,1.0);
+    P.xyz -= rotate_origin;
+    P = rotate_forward_matrix*P;
+    P.xyz += rotate_origin;
+    return P.xyz;
+}
+
+vec2 inverse(vec2 position)
+{
+    vec4 P = vec4(position,0.0,1.0);
+    P.xy -= rotate_origin.xy;
+    P = rotate_inverse_matrix*P;
+    P.xy += rotate_origin.xy;
+    return P.xy;
+}
+
+vec3 inverse(vec3 position)
+{
+    vec4 P = vec4(position,1.0);
+    P.xyz -= rotate_origin;
+    P = rotate_inverse_matrix*P;
+    P.xyz += rotate_origin;
+    return P.xyz;
+}
diff --git a/vispy/glsl/transforms/trackball.glsl b/vispy/glsl/transforms/trackball.glsl
new file mode 100644
index 0000000..2e4f783
--- /dev/null
+++ b/vispy/glsl/transforms/trackball.glsl
@@ -0,0 +1,15 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+uniform mat4 trackball_view;
+uniform mat4 trackball_model;
+uniform mat4 trackball_projection;
+
+vec4 transform(vec4 position)
+{
+    return trackball_projection
+           * trackball_view
+           * trackball_model
+           * position;
+}
diff --git a/vispy/glsl/transforms/translate.glsl b/vispy/glsl/transforms/translate.glsl
new file mode 100644
index 0000000..2386103
--- /dev/null
+++ b/vispy/glsl/transforms/translate.glsl
@@ -0,0 +1,35 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+uniform vec3 translate_translate;
+
+vec2 forward(float x, float y)
+{ return vec2(x,y) + translate_translate.xy; }
+
+vec2 forward(vec2 P)
+{ return P + translate_translate.xy; }
+
+vec3 forward(float x, float y, float z)
+{ return vec3(x,y,z) + translate_translate); }
+
+vec3 forward(vec3 P)
+{ return P + translate_translate; }
+
+vec4 forward(vec4 P)
+{ return vec4(P.xyz + translate_translate, P.w); }
+
+vec2 inverse(float x, float y)
+{ return vec2(x,y) - translate_translate.xy; }
+
+vec2 inverse(vec2 P)
+{ return P - translate_translate.xy; }
+
+vec3 inverse(float x, float y, float z)
+{ return vec3(x,y,z) - translate_translate); }
+
+vec3 inverse(vec3 P)
+{ return P - translate_translate; }
+
+vec4 inverse(vec4 P)
+{ return vec4(P.xyz - translate_translate, P.w); }
diff --git a/vispy/glsl/transforms/transverse_mercator.glsl b/vispy/glsl/transforms/transverse_mercator.glsl
new file mode 100644
index 0000000..cd3bd03
--- /dev/null
+++ b/vispy/glsl/transforms/transverse_mercator.glsl
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+//
+//   Transverse Mercator projection
+//   See http://en.wikipedia.org/wiki/Transverse_Mercator_projection
+//
+// ----------------------------------------------------------------------------
+#include "math/constants.glsl"
+
+// Constants
+const float k0 = 0.75;
+const float a  = 1.00;
+
+// Helper functions
+float cosh(float x) { return 0.5 * (exp(x)+exp(-x)); }
+float sinh(float x) { return 0.5 * (exp(x)-exp(-x)); }
+
+vec2 forward(float lambda, float phi)
+{
+    float x = 0.5*k0*log((1.0+sin(lambda)*cos(phi)) / (1.0 - sin(lambda)*cos(phi)));
+    float y = k0*a*atan(tan(phi), cos(lambda));
+    return vec2(x,y);
+}
+vec2 forward(vec2 P) { return forward(P.x,P.y); }
+vec3 forward(vec3 P) { return vec3(forward(P.x,P.y), P.z); }
+vec4 forward(vec4 P) { return vec4(forward(P.x,P.y), P.z, P.w); }
+
+vec2 inverse(float x, float y)
+{
+    float lambda = atan(sinh(x/(k0*a)),cos(y/(k0*a)));
+    float phi    = asin(sin(y/(k0*a))/cosh(x/(k0*a)));
+    return vec2(lambda,phi);
+}
+vec2 inverse(vec2 P) { return inverse(P.x,P.y); }
+vec3 inverse(vec3 P) { return vec3(inverse(P.x,P.y), P.z); }
+vec4 inverse(vec4 P) { return vec4(inverse(P.x,P.y), P.z, P.w); }
diff --git a/vispy/glsl/transforms/viewport-clipping.glsl b/vispy/glsl/transforms/viewport-clipping.glsl
new file mode 100644
index 0000000..b85879b
--- /dev/null
+++ b/vispy/glsl/transforms/viewport-clipping.glsl
@@ -0,0 +1,14 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+uniform vec4 viewport;    // in pixels
+uniform vec2 iResolution; // in pixels
+void clipping(void)
+{
+    vec2 position = gl_FragCoord.xy;
+         if( position.x < (viewport.x)) discard;
+    else if( position.x > (viewport.x+viewport.z)) discard;
+    else if( position.y < (viewport.y)) discard;
+    else if( position.y > (viewport.y+viewport.w)) discard;
+}
diff --git a/vispy/glsl/transforms/viewport-transform.glsl b/vispy/glsl/transforms/viewport-transform.glsl
new file mode 100644
index 0000000..dfa1e38
--- /dev/null
+++ b/vispy/glsl/transforms/viewport-transform.glsl
@@ -0,0 +1,16 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+uniform vec4 viewport;    // in pixels
+uniform vec2 iResolution; // in pixels
+vec4 transform(vec4 position)
+{
+    float w = viewport.z / iResolution.x;
+    float h = viewport.w / iResolution.y;
+    float x = 2.0*(viewport.x / iResolution.x) - 1.0 + w;
+    float y = 2.0*(viewport.y / iResolution.y) - 1.0 + h;
+    return  vec4((x + w*position.x/position.w)*position.w,
+                 (y + h*position.y/position.w)*position.w,
+                 position.z, position.w);
+}
diff --git a/vispy/glsl/transforms/viewport.glsl b/vispy/glsl/transforms/viewport.glsl
new file mode 100644
index 0000000..6b4eda9
--- /dev/null
+++ b/vispy/glsl/transforms/viewport.glsl
@@ -0,0 +1,50 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+uniform vec4 viewport_local;
+uniform vec4 viewport_global;
+uniform int viewport_transform;
+uniform int viewport_clipping;
+
+#ifdef __VERTEX_SHADER__
+void transform(void)
+{
+    if (viewport_transform == 0) return;
+
+    vec4 position = gl_Position;
+
+    float w = viewport_local.z / viewport_global.z;
+    float h = viewport_local.w / viewport_global.w;
+    float x = 2.0*(viewport_local.x / viewport_global.z) - 1.0 + w;
+    float y = 2.0*(viewport_local.y / viewport_global.w) - 1.0 + h;
+
+    gl_Position = vec4((x + w*position.x/position.w)*position.w,
+                       (y + h*position.y/position.w)*position.w,
+                       position.z, position.w);
+}
+#endif
+
+#ifdef __FRAGMENT_SHADER__
+void clipping(void)
+{
+//    if (viewport_clipping == 0) return;
+
+    vec2 position = gl_FragCoord.xy;
+         if( position.x < (viewport_local.x))                  discard;
+    else if( position.x > (viewport_local.x+viewport_local.z)) discard;
+    else if( position.y < (viewport_local.y))                  discard;
+    else if( position.y > (viewport_local.y+viewport_local.w)) discard;
+
+    /*
+    if( length(position.x - viewport_local.x) < 1.0 )
+        gl_FragColor = vec4(0,0,0,1);
+    else if( length(position.x - viewport_local.x - viewport_local.z) < 1.0 )
+        gl_FragColor = vec4(0,0,0,1);
+    else if( length(position.y - viewport_local.y) < 1.0 )
+        gl_FragColor = vec4(0,0,0,1);
+    else if( length(position.y - viewport_local.y - viewport_local.w) < 1.0 )
+        gl_FragColor = vec4(0,0,0,1);
+    */
+}
+#endif
diff --git a/vispy/glsl/transforms/x.glsl b/vispy/glsl/transforms/x.glsl
new file mode 100644
index 0000000..f537551
--- /dev/null
+++ b/vispy/glsl/transforms/x.glsl
@@ -0,0 +1,24 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+float get_x(float x)
+{
+    return x;
+}
+
+float get_x(vec2 xy)
+{
+    return xy.x;
+}
+
+float get_x(vec3 xyz)
+{
+    return xyz.x;
+}
+
+float get_x(vec4 xyzw)
+{
+    return xyzw.x;
+}
diff --git a/vispy/glsl/transforms/y.glsl b/vispy/glsl/transforms/y.glsl
new file mode 100644
index 0000000..d24d62a
--- /dev/null
+++ b/vispy/glsl/transforms/y.glsl
@@ -0,0 +1,19 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+float get_y(vec2 xy)
+{
+    return xy.y;
+}
+
+float get_y(vec3 xyz)
+{
+    return xyz.y;
+}
+
+float get_y(vec4 xyzw)
+{
+    return xyzw.y;
+}
diff --git a/vispy/glsl/transforms/z.glsl b/vispy/glsl/transforms/z.glsl
new file mode 100644
index 0000000..2b75f2f
--- /dev/null
+++ b/vispy/glsl/transforms/z.glsl
@@ -0,0 +1,14 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+// Distributed under the (new) BSD License.
+// ----------------------------------------------------------------------------
+
+float get_z(vec3 xyz)
+{
+    return xyz.z;
+}
+
+float get_z(vec4 xyzw)
+{
+    return xyzw.z;
+}
diff --git a/vispy/html/static/js/jquery.mousewheel.min.js b/vispy/html/static/js/jquery.mousewheel.min.js
new file mode 100644
index 0000000..bb7f43f
--- /dev/null
+++ b/vispy/html/static/js/jquery.mousewheel.min.js
@@ -0,0 +1,8 @@
+/*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh)
+ * Licensed under the MIT License (LICENSE.txt).
+ *
+ * Version: 3.1.12
+ *
+ * Requires: jQuery 1.2.2+
+ */
+!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){function b(b){var g=b||window.event,h=i.call(arguments,1),j=0,l=0,m=0,n=0,o=0,p=0;if(b=a.event.fix(g),b.type="mousewheel","detail"in g&&(m=-1*g.detail),"wheelDelta"in g&&(m=g.wheelDelta),"wheelDeltaY"in g&&(m=g.wheelDeltaY),"wheelDeltaX"in g&&(l=-1*g.wheelDeltaX),"axis"in g&&g.axis===g.HORIZONTAL_AXIS&&(l=-1*m,m=0),j=0===m?l:m,"deltaY"in g&&(m=-1*g.delt [...]
\ No newline at end of file
diff --git a/vispy/html/static/js/vispy.js b/vispy/html/static/js/vispy.js
deleted file mode 100644
index 81e75af..0000000
--- a/vispy/html/static/js/vispy.js
+++ /dev/null
@@ -1,190 +0,0 @@
-function get_pos(c, e) {
-    var rect = c.getBoundingClientRect();
-    return [e.clientX - rect.left, e.clientY - rect.top];
-};
-
-function get_modifiers(e) {
-    var modifiers = [];
-    if (e.altKey) modifiers.push("alt");
-    if (e.ctrlKey) modifiers.push("ctrl");
-    if (e.metaKey) modifiers.push("meta");
-    if (e.shiftKey) modifiers.push("shift");
-    return modifiers;
-};
-
-function get_key(e) {
-    var keynum = null;
-    if (window.event) { // IE
-        keynum = e.keyCode;
-    } else if (e.which) { // Netscape/Firefox/Opera
-        keynum = e.which;
-    }
-    return keynum;
-};
-
-function gen_mouse_event(c, e, type) {
-    if (c._eventinfo.is_button_pressed)
-        var button = e.button;
-    else
-        button = null;
-    var pos = get_pos(c, e);
-    var modifiers = get_modifiers(e);
-    var press_event = c._eventinfo.press_event;
-    var event = {
-        "source": "browser",
-        "event":
-        // Mouse Event
-        {
-            "name": "MouseEvent",
-            "properties": {
-                "type": type,
-                "pos": pos,
-                "button": e.button,
-                "is_dragging": press_event != null,
-                "modifiers": modifiers,
-                "press_event": press_event,
-                "delta": null,
-            }
-        }
-    };
-    return event;
-};
-
-function gen_key_event(c, e, type) {
-    var modifiers = get_modifiers(e);
-    var key_code = get_key(e);
-    var key_text = String.fromCharCode(key_code);
-    var event = {
-        "source": "browser",
-        "event":
-        // Key Event
-        {
-            "name": "KeyEvent",
-            "properties": {
-                "type": type,
-                "key": key_code,
-                "text": key_text,
-                "modifiers": modifiers,
-            }
-        }
-    };
-    return event;
-};
-
-function send_timer_event(w) {
-    var event = {
-        "event":
-        // Poll Event
-        {
-            "name": "PollEvent",
-        }
-    };
-    w.send(event);
-};
-
-require(["widgets/js/widget"], function(WidgetManager) {
-    var Widget = IPython.DOMWidgetView.extend({
-        render: function() {
-            this.$canvas = $('<canvas />')
-                .attr('id', 'canvas')
-                .attr('tabindex', '1')
-                .appendTo(this.$el);
-
-            this.c = this.$canvas[0];
-            this.c.width = this.model.get("width");
-            this.c.height = this.model.get("height");
-            this.canvas2d = this.c.getContext('2d');
-
-            this.c._eventinfo = {
-                'type': null,
-                'pos': null,
-                'button': null,
-                'is_dragging': null,
-                'key': null,
-                'modifiers': [],
-                'press_event': null,
-                'delta': [],
-                'is_button_pressed': 0,
-                'last_pos': [-1, -1],
-            };
-
-            this.c.interval = 50.0;  // Arbitrary for now
-            this.c.timer = setInterval(send_timer_event, this.c.interval, this);
-        },
-
-        events: {
-            'mousemove': 'mouse_move',
-            'mousedown': 'mouse_press',
-            'mouseup': 'mouse_release',
-            'mousewheel': 'mouse_wheel',
-            'keydown': 'key_press',
-            'keyup': 'key_release',
-        },
-
-        mouse_move: function(e) {
-            var event = gen_mouse_event(this.c, e, "mouse_move");
-            var pos = event.event.properties.pos;
-            var last_pos = this.c._eventinfo.last_pos;
-            if (pos[0] != last_pos[0] || pos[1] != last_pos[1])
-                this.send(event);
-            this.c._eventinfo.last_pos = pos;
-        },
-
-        mouse_press: function(e) {
-            ++this.c._eventinfo.is_button_pressed;
-            var event = gen_mouse_event(this.c, e, "mouse_press");
-            this.c._eventinfo.press_event = event.event.properties;
-            this.send(event);
-        },
-
-        mouse_release: function(e) {
-            --this.c._eventinfo.is_button_pressed;
-            var event = gen_mouse_event(this.c, e, "mouse_release");
-            this.c._eventinfo.press_event = null;
-            this.send(event);
-        },
-
-        mouse_wheel: function(e) {
-            var event = gen_mouse_event(this.c, e, "mouse_wheel");
-            var delta = [e.originalEvent.wheelDeltaX / 120, e.originalEvent.wheelDeltaY / 120];
-            event.event.properties.delta = delta;
-            this.send(event);
-            // Keep page from scrolling
-            e.preventDefault();
-        },
-
-        key_press: function(e) {
-            var event = gen_key_event(this.c, e, "key_press");
-            this.send(event);
-        },
-
-        key_release: function(e) {
-            var event = gen_key_event(this.c, e, "key_release");
-            this.send(event);
-        },
-
-        // Update, whenever value attribute of our widget changes
-        update: function() {
-            if(this.model.get("is_closing") == true)
-            {
-                clearInterval(this.c.timer);  // Remove existing timer
-                return;
-            }
-
-            this.c.width = this.model.get("width");
-            this.c.height = this.model.get("height");
-            var new_int = this.model.get("interval");
-            if(this.c.interval != new_int)  // Update the interval
-            {
-                this.c.interval = new_int;
-                clearInterval(this.c.timer);  // Remove existing and set new one
-                this.c.timer = setInterval(send_timer_event, this.c.interval, this);
-            }
-            var img_str = this.model.get("value");
-            var img = new Image();
-            img.src = "data:image/png;base64," + img_str;
-            this.canvas2d.drawImage(img, 0, 0);
-        },
-    })
-    WidgetManager.register_widget_view("Widget", Widget);
-});
diff --git a/vispy/html/static/js/vispy.min.js b/vispy/html/static/js/vispy.min.js
new file mode 100644
index 0000000..bceaa90
--- /dev/null
+++ b/vispy/html/static/js/vispy.min.js
@@ -0,0 +1,2 @@
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;"undefined"!=typeof window?t=window:"undefined"!=typeof global?t=global:"undefined"!=typeof self&&(t=self),t.vispy=e()}}(function(){var e;return function t(e,n,r){function i(u,s){if(!n[u]){if(!e[u]){var l="function"==typeof require&&require;if(!s&&l)return l(u,!0);if(o)return o(u,!0);var a=new Error("Cannot find module '"+u+"'");thr [...]
+//# sourceMappingURL=vispy.min.js.map
\ No newline at end of file
diff --git a/vispy/html/static/js/webgl-backend.js b/vispy/html/static/js/webgl-backend.js
new file mode 100644
index 0000000..493f3c0
--- /dev/null
+++ b/vispy/html/static/js/webgl-backend.js
@@ -0,0 +1,140 @@
+
+
+// VispyWidget code
+define(function(require) {
+    "use strict";
+
+    function _inline_glir_commands(commands, buffers) {
+        // Put back the buffers within the GLIR commands before passing them
+        // to the GLIR JavaScript interpretor.
+        for (var i = 0; i < commands.length; i++) {
+            var command = commands[i];
+            if (command[0] == 'DATA') {
+                var buffer_index = command[3]['buffer_index'];
+                command[3] = buffers[buffer_index];
+            }
+        }
+        return commands;
+    }
+
+    var vispy = require("/nbextensions/vispy/vispy.min.js");
+    var widget = require("widgets/js/widget");
+
+    var VispyView = widget.DOMWidgetView.extend({
+
+        initialize: function (parameters) {
+            VispyView.__super__.initialize.apply(this, [parameters]);
+
+            this.model.on('msg:custom', this.on_msg, this);
+
+            // Track canvas size changes.
+            this.model.on('change:width', this.size_changed, this);
+            this.model.on('change:height', this.size_changed, this);
+        },
+
+        render: function() {
+            var that = this;
+
+            var canvas = $('<canvas></canvas>');
+            // canvas.css('border', '1px solid rgb(171, 171, 171)');
+            canvas.css('background-color', '#000');
+            canvas.attr('tabindex', '1');
+            this.$el.append(canvas);
+            this.$canvas = canvas;
+
+            // Initialize the VispyCanvas.
+            this.c = vispy.init(canvas);
+
+            this.c.on_resize(function (e) {
+                that.model.set('width', e.size[0]);
+                that.model.set('height', e.size[1]);
+                that.touch();
+            });
+
+            // Start the event loop.
+            this.c.on_event_tick(function() {
+                // This callback function will be called at each JS tick,
+                // before the GLIR commands are flushed.
+
+                // Retrieve and flush the event queue.
+                var events = that.c.event_queue.get();
+
+                that.c.event_queue.clear();
+                // Send the events if the queue is not empty.
+                if (events.length > 0) {
+                    // Create the message.
+                    var msg = {
+                        msg_type: 'events',
+                        contents: events
+                    };
+                    // console.debug(events);
+                    // Send the message with the events to Python.
+                    that.send(msg);
+                }
+            });
+
+            vispy.start_event_loop();
+            var msg = { msg_type: 'init' };
+            this.send(msg);
+            // Make sure the size is correctly set up upon first display.
+            this.size_changed();
+            this.c.resize();
+            this.c.resizable();
+        },
+
+        on_msg: function(comm_msg) {
+            var buffers = comm_msg.buffers;
+            var msg = comm_msg; //.content.data.content;
+            if (msg == undefined) return;
+            // Receive and execute the GLIR commands.
+            if (msg.msg_type == 'glir_commands') {
+                var commands = msg.commands;
+                // Get the buffers messages.
+                if (msg.array_serialization == 'base64') {
+                    var buffers_msg = msg.buffers;
+                }
+                else if (msg.array_serialization == 'binary') {
+                    // Need to put the raw binary buffers in JavaScript
+                    // objects for the inline commands.
+                    var buffers_msg = [];
+                    for (var i = 0; i < buffers.length; i++) {
+                        buffers_msg[i] = {
+                            'storage_type': 'binary',
+                            'buffer': buffers[i]
+                        };
+                    }
+                }
+
+                // Make the GLIR commands ready for the JavaScript parser
+                // by inlining the buffers.
+                var commands_inlined = _inline_glir_commands(
+                    commands, buffers_msg);
+                for (var i = 0; i < commands_inlined.length; i++) {
+                    var command = commands[i];
+                    // Replace
+                    // console.debug(command);
+                    this.c.command(command);
+                }
+            }
+        },
+
+        // When the model's size changes.
+        size_changed: function() {
+            var size = [this.model.get('width'), this.model.get('height')];
+            this.$canvas.css('width', size[0] + 'px');
+            this.$canvas.css('height', size[1] + 'px');
+        },
+
+        remove: function() {
+            vispy.unregister(this.c);
+            // Inform Python that the widget has been removed.
+            this.send({
+                msg_type: 'status',
+                contents: 'removed'
+            });
+        }
+    });
+
+    //IPython.WidgetManager.register_widget_view('VispyView', VispyView);
+    return { 'VispyView' : VispyView };
+});
diff --git a/vispy/io/__init__.py b/vispy/io/__init__.py
index 4e93fc8..9bb74d1 100644
--- a/vispy/io/__init__.py
+++ b/vispy/io/__init__.py
@@ -1,13 +1,10 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 Utilities related to data reading, writing, fetching, and generation.
 """
 
-__all__ = ['imread', 'imsave', 'load_iris', 'load_crate',
-           'load_data_file', 'read_mesh', 'read_png', 'write_mesh',
-           'write_png']
 
 from os import path as _op
 
@@ -17,3 +14,7 @@ from .image import (read_png, write_png, imread, imsave, _make_png,  # noqa
                     _check_img_lib)  # noqa
 
 _data_dir = _op.join(_op.dirname(__file__), '_data')
+
+__all__ = ['imread', 'imsave', 'load_iris', 'load_crate',
+           'load_data_file', 'read_mesh', 'read_png', 'write_mesh',
+           'write_png']
diff --git a/vispy/io/datasets.py b/vispy/io/datasets.py
index f0b591e..f2f2579 100644
--- a/vispy/io/datasets.py
+++ b/vispy/io/datasets.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
 from os import path as op
 
-from ..util.fetching import load_data_file
+from ..util import load_data_file
 
 # This is the package data dir, not the dir for config, etc.
 DATA_DIR = op.join(op.dirname(__file__), '_data')
@@ -20,7 +20,8 @@ def load_iris():
         data['data'] : a (150, 4) NumPy array with the iris' features
         data['group'] : a (150,) NumPy array with the iris' group
     """
-    return np.load(load_data_file('iris/iris.npz'))
+    return np.load(load_data_file('iris/iris.npz',
+                                  force_download='2014-09-04'))
 
 
 def load_crate():
diff --git a/vispy/io/image.py b/vispy/io/image.py
index 99fa6a2..4d0c404 100644
--- a/vispy/io/image.py
+++ b/vispy/io/image.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 # Author: Luke Campagnola
diff --git a/vispy/io/mesh.py b/vispy/io/mesh.py
index d76cdce..5d41cce 100644
--- a/vispy/io/mesh.py
+++ b/vispy/io/mesh.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ Reading and writing of data like images and meshes.
diff --git a/vispy/io/tests/test_image.py b/vispy/io/tests/test_image.py
index e1d5220..e72be8e 100644
--- a/vispy/io/tests/test_image.py
+++ b/vispy/io/tests/test_image.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import numpy as np
 from numpy.testing import assert_array_equal, assert_allclose
@@ -7,7 +7,7 @@ from os import path as op
 import warnings
 
 from vispy.io import load_crate, imsave, imread, read_png, write_png
-from vispy.testing import requires_img_lib
+from vispy.testing import requires_img_lib, run_tests_if_main
 from vispy.util import _TempDir
 
 temp_dir = _TempDir()
@@ -43,3 +43,6 @@ def test_read_write_image():
     with warnings.catch_warnings(record=True):  # PIL unclosed file
         im2 = imread(fname)
     assert_allclose(im1, im2)
+
+
+run_tests_if_main()
diff --git a/vispy/io/tests/test_io.py b/vispy/io/tests/test_io.py
index 9be5a03..5006234 100644
--- a/vispy/io/tests/test_io.py
+++ b/vispy/io/tests/test_io.py
@@ -1,14 +1,14 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import numpy as np
 from os import path as op
-from nose.tools import assert_equal, assert_raises
 from numpy.testing import assert_allclose, assert_array_equal
 
 from vispy.io import write_mesh, read_mesh, load_data_file
 from vispy.geometry import _fast_cross_3d
 from vispy.util import _TempDir
+from vispy.testing import run_tests_if_main, assert_equal, assert_raises
 
 temp_dir = _TempDir()
 
@@ -69,3 +69,6 @@ def test_huge_cross():
     z = np.cross(x, y)
     zz = _fast_cross_3d(x, y)
     assert_array_equal(z, zz)
+
+
+run_tests_if_main()
diff --git a/vispy/io/wavefront.py b/vispy/io/wavefront.py
index ac270ae..6b91bc3 100644
--- a/vispy/io/wavefront.py
+++ b/vispy/io/wavefront.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 # This module was taken from visvis
diff --git a/vispy/mpl_plot/__init__.py b/vispy/mpl_plot/__init__.py
index 921eeb7..ffa7134 100644
--- a/vispy/mpl_plot/__init__.py
+++ b/vispy/mpl_plot/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
diff --git a/vispy/mpl_plot/_mpl_to_vispy.py b/vispy/mpl_plot/_mpl_to_vispy.py
index 0faa3e0..723318d 100644
--- a/vispy/mpl_plot/_mpl_to_vispy.py
+++ b/vispy/mpl_plot/_mpl_to_vispy.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
@@ -24,8 +24,8 @@ from ..io import read_png
 
 from ..scene.visuals import Line, Markers, Text, Image
 from ..scene.widgets import ViewBox
-from ..scene.transforms import STTransform
-from ..scene import SceneCanvas
+from ..visuals.transforms import STTransform
+from ..scene import SceneCanvas, PanZoomCamera
 from ..testing import has_matplotlib
 
 
@@ -66,8 +66,8 @@ class VispyRenderer(Renderer):
         vb = ViewBox(parent=self.canvas.scene, border_color='black',
                      bgcolor=props['axesbg'])
         vb.clip_method = 'fbo'  # necessary for bgcolor
-        vb.camera.rect = (xlim[0], ylim[0],
-                          xlim[1] - xlim[0], ylim[1] - ylim[0])
+        vb.camera = PanZoomCamera()
+        vb.camera.set_range(xlim, ylim, margin=0)
         ax_dict = dict(ax=ax, bounds=bounds, vb=vb, lims=xlim+ylim)
         self._axs[ax] = ax_dict
         self._resize(*self.canvas.size)
@@ -120,7 +120,7 @@ class VispyRenderer(Renderer):
         face_color.alpha = style['alpha']
         markers = Markers()
         markers.set_data(data, face_color=face_color, edge_color=edge_color,
-                         size=style['markersize'], style=style['marker'])
+                         size=style['markersize'], symbol=style['marker'])
         markers.parent = self._mpl_ax_to(mplobj).scene
 
     def draw_path(self, data, coordinates, pathcodes, style,
@@ -133,7 +133,7 @@ class VispyRenderer(Renderer):
         color = Color(style['edgecolor'])
         color.alpha = style['alpha']
         line = Line(data, color=color, width=style['edgewidth'],
-                    mode='gl')  # XXX Looks bad with agg :(
+                    method='gl')  # XXX Looks bad with agg :(
         line.parent = self._mpl_ax_to(mplobj).scene
 
     def _mpl_ax_to(self, mplobj, output='vb'):
diff --git a/vispy/mpl_plot/tests/test_show_vispy.py b/vispy/mpl_plot/tests/test_show_vispy.py
index 05d954e..fb8b4a6 100644
--- a/vispy/mpl_plot/tests/test_show_vispy.py
+++ b/vispy/mpl_plot/tests/test_show_vispy.py
@@ -1,12 +1,12 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
-from nose.tools import assert_raises
 
 from vispy.io import read_png, load_data_file
-from vispy.testing import has_matplotlib, requires_application
+from vispy.testing import (has_matplotlib, requires_application,
+                           run_tests_if_main, assert_raises)
 import vispy.mpl_plot as plt
 
 
@@ -28,3 +28,6 @@ def test_show_vispy():
         canvases[0].close()
     else:
         assert_raises(ImportError, plt.show)
+
+
+run_tests_if_main()
diff --git a/vispy/plot/__init__.py b/vispy/plot/__init__.py
index 2a8e2ec..b1697ed 100644
--- a/vispy/plot/__init__.py
+++ b/vispy/plot/__init__.py
@@ -1,11 +1,37 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
-[Experimental] This module provides functions for displaying data from a
-command-line interface.
+This module provides functions for displaying data from a command-line
+interface.
+
+**NOTE**: This module is still experimental, and under development.
+It currently lacks axes, but that is a high-priority target for
+the next release.
+
+Usage
+-----
+To use `vispy.plot` typically the main class `Fig` is first instantiated::
+
+    >>> from vispy.plot import Fig
+    >>> fig = Fig()
+
+And then `PlotWidget` instances are automatically created by accessing
+the ``fig`` instance::
+
+    >>> ax_left = fig[0, 0]
+    >>> ax_right = fig[0, 1]
+
+Then plots are accomplished via methods of the `PlotWidget` instances::
+
+    >>> import numpy as np
+    >>> data = np.random.randn(2, 10)
+    >>> ax_left.plot(data)
+    >>> ax_right.histogram(data[1])
+
 """
 
-__all__ = ['plot', 'image']
+from .fig import Fig
+from .plotwidget import PlotWidget
 
-from .plot import plot, image
+__all__ = ['Fig', 'PlotWidget']
diff --git a/vispy/plot/fig.py b/vispy/plot/fig.py
new file mode 100644
index 0000000..f271ec2
--- /dev/null
+++ b/vispy/plot/fig.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from ..scene import SceneCanvas
+from .plotwidget import PlotWidget
+
+
+class Fig(SceneCanvas):
+    """Create a figure window
+
+    Parameters
+    ----------
+    bgcolor : instance of Color
+        Color to use for the background.
+    size : tuple
+        Size of the figure window in pixels.
+    show : bool
+        If True, show the window.
+
+    Notes
+    -----
+    You can create a Figure, PlotWidget, and diagonal line plot like this::
+
+        >>> from vispy.plot import Fig
+        >>> fig = Fig()
+        >>> ax = fig[0, 0]  # this creates a PlotWidget
+        >>> ax.plot([[0, 1], [0, 1]])
+
+    See the gallery for many other examples.
+
+    See Also
+    --------
+    PlotWidget : the axis widget for plotting
+    SceneCanvas : the super class
+    """
+    def __init__(self, bgcolor='w', size=(800, 600), show=True):
+        super(Fig, self).__init__(bgcolor=bgcolor, keys='interactive',
+                                  show=show, size=size)
+        self._grid = self.central_widget.add_grid()
+        self._grid._default_class = PlotWidget
+        self._plot_widgets = []
+
+    @property
+    def plot_widgets(self):
+        """List of the associated PlotWidget instances"""
+        return tuple(self._plot_widgets)
+
+    def __getitem__(self, idxs):
+        """Get an axis"""
+        pw = self._grid.__getitem__(idxs)
+        self._plot_widgets += [pw]
+        return pw
diff --git a/vispy/plot/plot.py b/vispy/plot/plot.py
deleted file mode 100644
index 7348270..0000000
--- a/vispy/plot/plot.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from ..scene import SceneCanvas, visuals
-
-plots = []
-
-
-def plot(*args, **kwds):
-    """ Create a new canvas and plot the given data. 
-    
-    For arguments, see scene.visuals.LinePlot.
-    """
-    canvas = SceneCanvas(keys='interactive')
-    canvas.view = canvas.central_widget.add_view()
-    line = visuals.LinePlot(*args, **kwds)
-    canvas.view.add(line)
-    canvas.view.camera.auto_zoom(line)
-    canvas.show()
-    plots.append(canvas)
-    return canvas
-
-
-def image(*args, **kwds):
-    """ Create a new canvas and display the given image data.
-    
-    For arguments, see scene.visuals.Image.
-    """
-    canvas = SceneCanvas(keys='interactive')
-    canvas.view = canvas.central_widget.add_view()
-    image = visuals.Image(*args, **kwds)
-    canvas.view.add(image)
-    canvas.show()
-    canvas.view.camera.auto_zoom(image)
-    plots.append(canvas)
-    return canvas
diff --git a/vispy/plot/plotwidget.py b/vispy/plot/plotwidget.py
new file mode 100644
index 0000000..ce2d959
--- /dev/null
+++ b/vispy/plot/plotwidget.py
@@ -0,0 +1,259 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from ..scene import (Image, LinePlot, Volume, Mesh, Histogram,
+                     Spectrogram, ViewBox, PanZoomCamera, TurntableCamera)
+from ..io import read_mesh
+from ..geometry import MeshData
+
+__all__ = ['PlotWidget']
+
+
+class PlotWidget(ViewBox):
+    """Widget to facilitate plotting
+
+    Parameters
+    ----------
+    *args : arguments
+        Arguments passed to the `ViewBox` super class.
+    **kwargs : keywoard arguments
+        Keyword arguments passed to the `ViewBox` super class.
+
+    Notes
+    -----
+    This class is typically instantiated implicitly by a `Figure`
+    instance, e.g., by doing ``fig[0, 0]``.
+
+    See Also
+    --------
+    """
+    def __init__(self, *args, **kwargs):
+        super(PlotWidget, self).__init__(*args, **kwargs)
+        self._camera_set = False
+
+    def _set_camera(self, cls, *args, **kwargs):
+        if not self._camera_set:
+            self._camera_set = True
+            self.camera = cls(*args, **kwargs)
+            self.camera.set_range(margin=0)
+
+    def histogram(self, data, bins=10, color='w', orientation='h'):
+        """Calculate and show a histogram of data
+
+        Parameters
+        ----------
+        data : array-like
+            Data to histogram. Currently only 1D data is supported.
+        bins : int | array-like
+            Number of bins, or bin edges.
+        color : instance of Color
+            Color of the histogram.
+        orientation : {'h', 'v'}
+            Orientation of the histogram.
+
+        Returns
+        -------
+        hist : instance of Polygon
+            The histogram polygon.
+        """
+        hist = Histogram(data, bins, color, orientation)
+        self.add(hist)
+        self._set_camera(PanZoomCamera)
+        return hist
+
+    def image(self, data, cmap='cubehelix', clim='auto'):
+        """Show an image
+
+        Parameters
+        ----------
+        data : ndarray
+            Should have shape (N, M), (N, M, 3) or (N, M, 4).
+        cmap : str
+            Colormap name.
+        clim : str | tuple
+            Colormap limits. Should be ``'auto'`` or a two-element tuple of
+            min and max values.
+
+        Returns
+        -------
+        image : instance of Image
+            The image.
+
+        Notes
+        -----
+        The colormap is only used if the image pixels are scalars.
+        """
+        image = Image(data, cmap=cmap, clim=clim)
+        self.add(image)
+        self._set_camera(PanZoomCamera, aspect=1)
+        return image
+
+    def mesh(self, vertices=None, faces=None, vertex_colors=None,
+             face_colors=None, color=(0.5, 0.5, 1.), fname=None,
+             meshdata=None):
+        """Show a 3D mesh
+
+        Parameters
+        ----------
+        vertices : array
+            Vertices.
+        faces : array | None
+            Face definitions.
+        vertex_colors : array | None
+            Vertex colors.
+        face_colors : array | None
+            Face colors.
+        color : instance of Color
+            Color to use.
+        fname : str | None
+            Filename to load. If not None, then vertices, faces, and meshdata
+            must be None.
+        meshdata : MeshData | None
+            Meshdata to use. If not None, then vertices, faces, and fname
+            must be None.
+
+        Returns
+        -------
+        mesh : instance of Mesh
+            The mesh.
+        """
+        if fname is not None:
+            if not all(x is None for x in (vertices, faces, meshdata)):
+                raise ValueError('vertices, faces, and meshdata must be None '
+                                 'if fname is not None')
+            vertices, faces = read_mesh(fname)[:2]
+        if meshdata is not None:
+            if not all(x is None for x in (vertices, faces, fname)):
+                raise ValueError('vertices, faces, and fname must be None if '
+                                 'fname is not None')
+        else:
+            meshdata = MeshData(vertices, faces)
+        mesh = Mesh(meshdata=meshdata, vertex_colors=vertex_colors,
+                    face_colors=face_colors, color=color, shading='smooth')
+        self.add(mesh)
+        self._set_camera(TurntableCamera, azimuth=0, elevation=0)
+        return mesh
+
+    def plot(self, data, color='k', symbol='o', line_kind='-', width=1.,
+             marker_size=0., edge_color='k', face_color='k', edge_width=1.):
+        """Plot a series of data using lines and markers
+
+        Parameters
+        ----------
+        data : array | two arrays
+            Arguments can be passed as ``(Y,)``, ``(X, Y)`` or
+            ``np.array((X, Y))``.
+        color : instance of Color
+            Color of the line.
+        symbol : str
+            Marker symbol to use.
+        line_kind : str
+            Kind of line to draw. For now, only solid lines (``'-'``)
+            are supported.
+        width : float
+            Line width.
+        marker_size : float
+            Marker size. If `size == 0` markers will not be shown.
+        edge_color : instance of Color
+            Color of the marker edge.
+        face_color : instance of Color
+            Color of the marker face.
+        edge_width : float
+            Edge width of the marker.
+
+        Returns
+        -------
+        line : instance of LinePlot
+            The line plot.
+
+        See also
+        --------
+        marker_types, LinePlot
+        """
+        line = LinePlot(data, connect='strip', color=color, symbol=symbol,
+                        line_kind=line_kind, width=width,
+                        marker_size=marker_size, edge_color=edge_color,
+                        face_color=face_color, edge_width=edge_width)
+        self.add(line)
+        self._set_camera(PanZoomCamera)
+        return line
+
+    def spectrogram(self, x, n_fft=256, step=None, fs=1., window='hann',
+                    color_scale='log', cmap='cubehelix', clim='auto'):
+        """Calculate and show a spectrogram
+
+        Parameters
+        ----------
+        x : array-like
+            1D signal to operate on. ``If len(x) < n_fft``, x will be
+            zero-padded to length ``n_fft``.
+        n_fft : int
+            Number of FFT points. Much faster for powers of two.
+        step : int | None
+            Step size between calculations. If None, ``n_fft // 2``
+            will be used.
+        fs : float
+            The sample rate of the data.
+        window : str | None
+            Window function to use. Can be ``'hann'`` for Hann window, or None
+            for no windowing.
+        color_scale : {'linear', 'log'}
+            Scale to apply to the result of the STFT.
+            ``'log'`` will use ``10 * log10(power)``.
+        cmap : str
+            Colormap name.
+        clim : str | tuple
+            Colormap limits. Should be ``'auto'`` or a two-element tuple of
+            min and max values.
+
+        Returns
+        -------
+        spec : instance of Spectrogram
+            The spectrogram.
+
+        See also
+        --------
+        Image
+        """
+        # XXX once we have axes, we should use "fft_freqs", too
+        spec = Spectrogram(x, n_fft, step, fs, window,
+                           color_scale, cmap, clim)
+        self.add(spec)
+        self._set_camera(PanZoomCamera)
+        return spec
+
+    def volume(self, vol, clim=None, method='mip', threshold=None,
+               cmap='grays'):
+        """Show a 3D volume
+
+        Parameters
+        ----------
+        vol : ndarray
+            Volume to render.
+        clim : tuple of two floats | None
+            The contrast limits. The values in the volume are mapped to
+            black and white corresponding to these values. Default maps
+            between min and max.
+        method : {'mip', 'iso', 'translucent', 'additive'}
+            The render style to use. See corresponding docs for details.
+            Default 'mip'.
+        threshold : float
+            The threshold to use for the isosurafce render style. By default
+            the mean of the given volume is used.
+        cmap : str
+            The colormap to use.
+
+        Returns
+        -------
+        volume : instance of Volume
+            The volume visualization.
+
+        See also
+        --------
+        Volume
+        """
+        volume = Volume(vol, clim, method, threshold, cmap=cmap)
+        self.add(volume)
+        self._set_camera(TurntableCamera, fov=30.)
+        return volume
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/plot/tests/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/plot/tests/__init__.py
diff --git a/vispy/plot/tests/test_plot.py b/vispy/plot/tests/test_plot.py
new file mode 100644
index 0000000..0e1a093
--- /dev/null
+++ b/vispy/plot/tests/test_plot.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+import vispy.plot as vp
+from vispy.testing import (assert_raises, requires_application,
+                           run_tests_if_main)
+
+
+ at requires_application()
+def test_figure_creation():
+    """Test creating a figure"""
+    with vp.Fig(show=False) as fig:
+        fig[0, 0:2]
+        fig[1:3, 0:2]
+        ax_right = fig[1:3, 2]
+        assert fig[1:3, 2] is ax_right
+        # collision
+        assert_raises(ValueError, fig.__getitem__, (slice(1, 3), 1))
+
+run_tests_if_main()
diff --git a/vispy/scene/__init__.py b/vispy/scene/__init__.py
index 1f47ad6..c92810d 100644
--- a/vispy/scene/__init__.py
+++ b/vispy/scene/__init__.py
@@ -1,57 +1,44 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
-The vispy.scene namespace provides functionality for higher level
-visuals as well as scenegraph and related classes.
+The vispy.scene subpackage provides high-level, flexible, and easy to use
+functionality for creating scenes composed of multiple visual objects. 
 
+Overview
+--------
 
-Terminology
------------
+Scenegraphs are a commonly used system for describing a scene as a 
+hierarchy of visual objects. Users need only create these visual objects and 
+specify their location in the scene, and the scenegraph system will 
+automatically draw the entire scene whenever an update is required.
 
-* **entity** - an object that lives in the scenegraph. It can have zero or
-  more children and zero or more parents (although one is recommended).
-  It also has a transform that maps the local coordinate frame to the
-  coordinate frame of the parent.
+Using the vispy scenegraph requires only a few steps:
 
-* **scene** - a complete connected graph of entities.
+1. Create a SceneCanvas to display the scene. This object has a `scene` 
+   property that is the top-level Node in the scene.
+2. Create one or more Node instances (see vispy.scene.visuals)
+3. Add these Node instances to the scene by making them children of 
+   canvas.scene, or children of other nodes that are already in the scene.
 
-* **subscene** - the entities that are children of a viewbox. Any viewboxes
-  inside this subscene are part of the subscene, but not their children.
-  The SubScene class is the toplevel entity for any subscene. Each
-  subscene has its own camera, lights, aspect ratio, etc.
 
-* **visual** - an entity that has a visual representation. It can be made
-  visible/invisible and also has certain bounds.
+For more information see:
 
-* **widget** - an entity of a certain size that provides interaction. It
-  is made to live in a 2D scene with a pixel camera.
-
-* **viewbox** - an entity that provides a rectangular window to which a
-  subscene is rendered. Clipping is performed in one of several ways.
-
-* **camera** - an entity that specifies how the subscene of a viewbox is
-  rendered to the pixel grid. It determines position and orientation
-  (through its transform) an projection (through a special
-  transformation property). Some cameras also provide interaction (e.g.
-  zooming). Although there can be multiple cameras in a subscene, each
-  subscene has one active camera.
-
-* **viewport** - as in glViewPort, a sub pixel grid in a framebuffer.
-
-* **drawing system** - a part of the viewbox that takes care of rendering
-  a subscene to the pixel grid of that viewbox.
+* complete scenegraph documentation
+* scene examples
+* scene API reference
 
 """
 
-__all__ = ['SceneCanvas', 'Entity']
+__all__ = ['SceneCanvas', 'Node']
 
-from .entity import Entity  # noqa
+from .visuals import *  # noqa
+from .cameras import *  # noqa
+from ..visuals.transforms import *  # noqa
+from .widgets import *  # noqa
 from .canvas import SceneCanvas  # noqa
 from . import visuals  # noqa
+from ..visuals import transforms  # noqa
 from . import widgets  # noqa
 from . import cameras  # noqa
-from .visuals import *  # noqa
-from .cameras import *  # noqa
-from .transforms import *  # noqa
-from .widgets import *  # noqa
+from .node import Node  # noqa
diff --git a/vispy/scene/cameras.py b/vispy/scene/cameras.py
deleted file mode 100644
index a76a23f..0000000
--- a/vispy/scene/cameras.py
+++ /dev/null
@@ -1,595 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-"""
-A brief explanation of how cameras work 
----------------------------------------
-
-A Camera is responsible for setting the transform of a SubScene object such 
-that a certain part of the scene is mapped to the bounding rectangle of the 
-ViewBox. 
-
-The view of a camera is determined by its transform (that it has as
-being an entity) and its projection. The former is essentially the
-position and orientation of the camera, the latter determines field of
-view and any non-linear transform (such as perspective).
-
-"""
-from __future__ import division
-
-import numpy as np
-
-from .entity import Entity
-from ..geometry import Rect
-from .transforms import (STTransform, PerspectiveTransform, NullTransform,
-                         AffineTransform)
-
-
-def make_camera(cam_type, *args, **kwds):
-    """ Factory function for creating new cameras. 
-    
-    Parameters
-    ----------
-    cam_type : str
-        May be one of:
-            * 'panzoom' : Creates :class:`PanZoomCamera`
-            * 'turntable' : Creates :class:`TurntableCamera`
-            * None : Creates :class:`Camera`
-
-    All extra arguments are passed to the __init__ method of the selected
-    Camera class.
-    """
-    cam_types = {
-        None: BaseCamera,
-        'panzoom': PanZoomCamera,
-        'turntable': TurntableCamera,
-    }
-    
-    try: 
-        return cam_types[cam_type](*args, **kwds)
-    except KeyError:
-        raise KeyError('Unknown camera type "%s". Options are: %s' % 
-                       (cam_type, cam_types.keys()))
-
-
-class BaseCamera(Entity):
-    """ Camera describes the perspective from which a ViewBox views its 
-    subscene, and the way that user interaction affects that perspective.
-    
-    Most functionality is implemented in subclasses. This base class has
-    no user interaction and causes the subscene to use the same coordinate
-    system as the ViewBox.
-
-    Parameters
-    ----------
-    parent : Entity
-        The parent of the camera.
-    name : str
-        Name used to identify the camera in the scene.
-    """
-    def __init__(self, **kwargs):
-        self._viewbox = None
-        self._interactive = True
-        super(BaseCamera, self).__init__(**kwargs)
-        self.transform = NullTransform()
-
-    @property
-    def interactive(self):
-        """ Boolean describing whether the camera should enable or disable
-        user interaction.
-        """
-        return self._interactive
-    
-    @interactive.setter
-    def interactive(self, b):
-        self._interactive = b
-
-    @property
-    def viewbox(self):
-        """ The ViewBox that this Camera is attached to.        
-        """
-        return self._viewbox
-    
-    @viewbox.setter
-    def viewbox(self, vb):
-        if self._viewbox is not None:
-            self.disconnect()
-        self._viewbox = vb
-        if self._viewbox is not None:
-            self.connect()
-            self.parent = vb.scene
-        self._update_transform()
-    
-    def connect(self):
-        self._viewbox.events.mouse_press.connect(self.view_mouse_event)
-        self._viewbox.events.mouse_release.connect(self.view_mouse_event)
-        self._viewbox.events.mouse_move.connect(self.view_mouse_event)
-        self._viewbox.events.mouse_wheel.connect(self.view_mouse_event)
-        self._viewbox.events.resize.connect(self.view_resize_event)
-    
-    def disconnect(self):
-        self._viewbox.events.mouse_press.disconnect(self.view_mouse_event)
-        self._viewbox.events.mouse_release.disconnect(self.view_mouse_event)
-        self._viewbox.events.mouse_move.disconnect(self.view_mouse_event)
-        self._viewbox.events.mouse_wheel.disconnect(self.view_mouse_event)
-        self._viewbox.events.resize.disconnect(self.view_resize_event)
-    
-    def view_mouse_event(self, event):
-        """
-        The ViewBox received a mouse event; update transform 
-        accordingly.
-        """
-        pass
-        
-    def view_resize_event(self, event):
-        """
-        The ViewBox was resized; update the transform accordingly.
-        """
-        pass
-    
-    def _update_transform(self):
-        """ Subclasses should reimplement this method to update the scene
-        transform by calling self._set_scene_transform.
-        """
-        self._set_scene_transform(self.transform)
-        
-    def _set_scene_transform(self, tr):
-        """ Called by subclasses to configure the viewbox scene transform.
-        """
-        # todo: check whether transform has changed, connect to 
-        # transform.changed event
-        self._scene_transform = tr
-        if self.viewbox is not None:
-            self.viewbox.scene.transform = self._scene_transform
-            self.viewbox.update()
-
-    
-class PanZoomCamera(BaseCamera):
-    """
-    Camera implementing 2D pan/zoom mouse interaction. Primarily intended for
-    displaying plot data.
-
-    By default, this camera inverts the y axis of the scene. This usually 
-    results in the scene +y axis pointing upward because widgets (including 
-    ViewBox) have their +y axis pointing downward.
-    
-    User interaction:
-    
-    * Dragging left mouse button pans the view
-    * Dragging right mouse button vertically zooms the view y-axis
-    * Dragging right mouse button horizontally zooms the view x-axis
-    * Mouse wheel zooms both view axes equally.
-
-    Parameters
-    ----------
-    parent : Entity
-        The parent of the camera.
-    name : str
-        Name used to identify the camera in the scene.
-    """
-    def __init__(self, **kwargs):
-        super(PanZoomCamera, self).__init__(**kwargs)
-        self._rect = Rect((0, 0), (1, 1))  # visible range in scene
-        self._invert = [False, True]
-        self.transform = STTransform()
-        
-    def zoom(self, zoom, center):
-        """ Zoom the view around a center point.
-        
-        Parameters
-        ----------
-        zoom : length-2 sequence
-            The fraction to zoom the x and y axes.
-        center : length-2 sequence
-            The point (in the coordinate system of the scene) that will remain
-            stationary in the ViewBox while zooming.
-        """
-        # TODO: would be nice if STTransform had a nice scale(s, center) 
-        # method like AffineTransform.
-        transform = (STTransform(translate=center) * 
-                     STTransform(scale=zoom) * 
-                     STTransform(translate=-center))
-        
-        self.rect = transform.map(self.rect)
-        
-    def pan(self, pan):
-        """ Pan the view.
-        
-        Parameters
-        ----------
-        pan : length-2 sequence
-            The distance to pan the view, in the coordinate system of the 
-            scene.
-        """
-        self.rect = self.rect + pan
-
-    def auto_zoom(self, visual=None, padding=0.1):
-        """ Automatically configure the camera to fit a visual inside the
-        visible region.
-        """
-        bx = visual.bounds('visual', 0)
-        by = visual.bounds('visual', 1)
-        bounds = self.rect
-        if bx is not None:
-            bounds.left = bx[0]
-            bounds.right = bx[1]
-        if by is not None:
-            bounds.bottom = by[0]
-            bounds.top = by[1]
-            
-        if padding != 0:
-            pw = bounds.width * padding * 0.5
-            ph = bounds.height * padding * 0.5
-            bounds.left = bounds.left - pw
-            bounds.right = bounds.right + pw
-            bounds.top = bounds.top + ph
-            bounds.bottom = bounds.bottom - ph
-        self.rect = bounds
-
-    @property
-    def rect(self):
-        """ The rectangular border of the ViewBox visible area, expressed in
-        the coordinate system of the scene.
-        
-        By definition, the +y axis of this rect is opposite the +y axis of the
-        ViewBox. 
-        """
-        return self._rect
-        
-    @rect.setter
-    def rect(self, args):
-        """
-        Set the bounding rect of the visible area in the subscene. 
-        
-        By definition, the +y axis of this rect is opposite the +y axis of the
-        ViewBox. 
-        """
-        if isinstance(args, tuple):
-            self._rect = Rect(*args)
-        else:
-            self._rect = Rect(args)
-        self._update_transform()
-
-    @property 
-    def invert_y(self):
-        """ Boolean indicating whether the y axis of the SubScene is inverted 
-        relative to the ViewBox.
-        
-        Default is True--this camera inverts the y axis of the scene. In most
-        cases, this results in the scene +y axis pointing upward because 
-        widgets (including ViewBox) have their +y axis pointing downward.
-        """
-        return self._invert[1]
-    
-    @invert_y.setter
-    def invert_y(self, inv):
-        if not isinstance(inv, bool):
-            raise TypeError("Invert must be boolean.")
-        self._invert[1] = inv
-        self._update_transform()
-        
-    def view_resize_event(self, event):
-        self._update_transform()
-
-    def view_mouse_event(self, event):
-        """
-        The SubScene received a mouse event; update transform 
-        accordingly.
-        """
-        if event.handled or not self.interactive:
-            return
-        
-        if event.type == 'mouse_wheel':
-            scale = 1.1 ** -event.delta[1]
-            center = self._scene_transform.imap(event.pos[:2])
-            self.zoom((scale, scale), center)
-            event.handled = True
-            
-        elif event.type == 'mouse_move':
-            if 1 in event.buttons:
-                p1 = np.array(event.last_event.pos)[:2]
-                p2 = np.array(event.pos)[:2]
-                p1s = self._scene_transform.imap(p1)
-                p2s = self._scene_transform.imap(p2)
-                self.pan(p1s-p2s)
-                event.handled = True
-            elif 2 in event.buttons:
-                # todo: just access the original event position, rather
-                # than mapping to the viewbox and back again.
-                p1 = np.array(event.last_event.pos)[:2]
-                p2 = np.array(event.pos)[:2]
-                p1c = event.map_to_canvas(p1)[:2]
-                p2c = event.map_to_canvas(p2)[:2]
-                
-                scale = 1.03 ** ((p1c-p2c) * np.array([1, -1]))
-                center = self._scene_transform.imap(event.press_event.pos[:2])
-                
-                self.zoom(scale, center)
-                event.handled = True
-        
-        if event.handled:
-            self._update_transform()
-
-    def _update_transform(self):
-        if self.viewbox is None:
-            return
-        
-        vbr = self.viewbox.rect.flipped(x=self._invert[0], y=self._invert[1])
-        self.transform.set_mapping(self.rect, vbr)
-        self._set_scene_transform(self.transform)
-
-        
-class PerspectiveCamera(BaseCamera):
-    """ Base class for 3D cameras supporting orthographic and perspective
-    projections.
-    
-    User interaction:
-    
-    * Dragging left mouse button orbits the view around its center point.
-    * Mouse wheel changes the field of view angle.
-    
-    Parameters
-    ----------
-    mode : str
-        Perspective mode, either 'ortho' or 'perspective'.
-    fov : float
-        Field of view.
-    width : float
-        Width.
-    parent : Entity
-        The parent of the camera.
-    name : str
-        Name used to identify the camera in the scene.
-    """
-    def __init__(self, mode='ortho', fov=60., width=10., **kwargs):
-        # projection transform and associated options
-        self._projection = PerspectiveTransform()
-        self._mode = None
-        self._fov = None
-        self._width = None
-
-        super(PerspectiveCamera, self).__init__(**kwargs)
-
-        self.mode = mode
-        self.fov = fov
-        self.width = width
-        
-        # camera transform
-        self.transform = AffineTransform()
-        
-    @property
-    def mode(self):
-        """ Describes the current projection mode of the camera. 
-        
-        May be 'ortho' or 'perspective'. In orthographic mode, objects appear 
-        to have constant size regardless of their distance from the camera.
-        In perspective mode, objects appear smaller as they are farther from 
-        the camera.
-        """
-        return self._mode
-    
-    @mode.setter
-    def mode(self, mode):
-        if mode == 'ortho':
-            self._near = -1e3
-        elif mode == 'perspective':
-            self._near = 1e-2
-        else:
-            raise ValueError('Accepted modes are "ortho" and "perspective".')
-        self._far = 1e6
-        self._mode = mode
-        
-        self._update_transform()
-        
-    @property
-    def fov(self):
-        """ Field-of-view angle of the camera when in perspective mode.
-        """
-        return self._fov
-    
-    @fov.setter
-    def fov(self, fov):
-        if fov < 0 or fov >= 180:
-            raise ValueError("fov must be between 0 and 180.")
-        self._fov = fov
-        self._update_transform()
-        
-    @property
-    def width(self):
-        """ Width of the visible region when in orthographic mode.
-        """
-        return self._width
-    
-    @width.setter
-    def width(self, width):
-        self._width = width
-        self._update_transform()
-        
-    def view_resize_event(self, event):
-        self._update_transform()
-    
-    def _update_transform(self, event=None):
-        if self.viewbox is None:
-            return
-        
-        # configure projection transform
-        if self.mode == 'ortho': 
-            self.set_ortho()
-        elif self.mode == 'perspective':
-            self.set_perspective()
-        else:
-            raise ValueError("Unknown projection mode '%s'" % self.mode)
-        
-        # assemble complete transform mapping to viewbox bounds
-        unit = [[-1, 1], [1, -1]]
-        vrect = [[0, 0], self.viewbox.size]
-        viewbox_tr = STTransform.from_mapping(unit, vrect)
-        proj_tr = self._projection
-        cam_tr = self.entity_transform(self.viewbox.scene)
-        
-        tr = viewbox_tr * proj_tr * cam_tr
-        self._set_scene_transform(tr)
-
-    def set_perspective(self):
-        """ Set perspective projection matrix.
-        """
-        vbs = self.viewbox.size
-        ar = vbs[0] / vbs[1]
-        self._projection.set_perspective(self.fov, ar, self._near, self._far)
-
-    def set_ortho(self):
-        """ Set orthographic projection matrix.
-        """
-        vbs = self.viewbox.size
-        w = self.width / 2.
-        h = w * (vbs[1] / vbs[0])
-        self._projection.set_ortho(-w, w, -h, h, self._near, self._far)
-
-
-class TurntableCamera(PerspectiveCamera):
-    """ 3D camera class that orbits around a center point while maintaining a
-    fixed vertical orientation.
-
-    Parameters
-    ----------
-    elevation : float
-        Elevation in degrees.
-    azimuth : float
-        Azimuth in degrees.
-    distance : float
-        Distance away from the center.
-    center : array-like
-        3-element array defining the center point.
-    parent : Entity
-        The parent of the camera.
-    name : str
-        Name used to identify the camera in the scene.
-    """
-    def __init__(self, elevation=30., azimuth=30.,
-                 distance=10., center=(0, 0, 0), up='z', **kwds):
-        super(TurntableCamera, self).__init__(**kwds)
-        self.elevation = elevation
-        self.azimuth = azimuth
-        self.distance = distance
-        self.center = center
-        self.up = up
-        self._update_camera_pos()
-    
-    @property
-    def elevation(self):
-        """ The angle of the camera in degrees above the horizontal (x, z) 
-        plane.
-        """
-        return self._elevation
-
-    @elevation.setter
-    def elevation(self, elev):
-        self._elevation = elev
-        self._update_transform()
-    
-    @property
-    def azimuth(self):
-        """ The angle of the camera in degrees around the y axis. An angle of
-        0 places the camera within the (y, z) plane.
-        """
-        return self._azimuth
-
-    @azimuth.setter
-    def azimuth(self, azim):
-        self._azimuth = azim
-        self._update_transform()
-    
-    @property
-    def distance(self):
-        """ The distance from the camera to its center point.
-        """
-        return self._distance
-
-    @distance.setter
-    def distance(self, dist):
-        self._distance = dist
-        self._update_transform()
-    
-    @property
-    def center(self):
-        """ The position of the turntable center. This is the point around 
-        which the camera orbits.
-        """
-        return self._center
-
-    @center.setter
-    def center(self, center):
-        self._center = center
-        self._update_transform()
-    
-    def orbit(self, azim, elev):
-        """ Orbits the camera around the center position.
-        
-        Parameters
-        ----------
-        azim : float
-            Angle in degrees to rotate horizontally around the center point.
-        elev : float
-            Angle in degrees to rotate vertically around the center point.
-        """
-        self.azimuth += azim
-        self.elevation = np.clip(self.elevation + elev, -90, 90)
-        self._update_camera_pos()
-        
-    def view_mouse_event(self, event):
-        """
-        The viewbox received a mouse event; update transform 
-        accordingly.
-        """
-        if event.handled or not self.interactive:
-            return
-        
-        if event.type == 'mouse_wheel':
-            s = 1.1 ** -event.delta[1]
-            if self.mode == 'ortho':
-                self.width *= s
-            else:
-                self.fov = np.clip(self.fov * s, 0, 179)
-            self._update_camera_pos()
-        
-        elif event.type == 'mouse_move' and 1 in event.buttons:
-            p1 = np.array(event.last_event.pos)[:2]
-            p2 = np.array(event.pos)[:2]
-            p1c = event.map_to_canvas(p1)[:2]
-            p2c = event.map_to_canvas(p2)[:2]
-            d = p2c - p1c
-            self.orbit(-d[0], d[1])
-
-    def _update_camera_pos(self):
-        """ Set the camera position / orientation based on elevation,
-        azimuth, distance, and center properties.
-        """
-        # transform will be updated several times; do not update camera
-        # transform until we are done.
-        ch_em = self.events.transform_change
-        with ch_em.blocker(self._update_transform):
-            tr = self.transform
-            tr.reset()
-            if self.up == 'y':
-                tr.translate((0.0, 0.0, -self.distance))
-                tr.rotate(self.elevation, (-1, 0, 0))
-                tr.rotate(self.azimuth, (0, 1, 0))
-            elif self.up == 'z':
-                tr.rotate(90, (1, 0, 0))
-                tr.translate((0.0, -self.distance, 0.0))
-                tr.rotate(self.elevation, (-1, 0, 0))
-                tr.rotate(self.azimuth, (0, 0, 1))
-            else:
-                raise ValueError('TurntableCamera.up must be "y" or "z".')
-                
-            tr.translate(-np.array(self.center))
-        self._update_transform()
-
-
-#class ArcballCamera(PerspectiveCamera):
-#    pass
-
-
-#class FirstPersonCamera(PerspectiveCamera):
-#    pass
diff --git a/vispy/scene/cameras/__init__.py b/vispy/scene/cameras/__init__.py
new file mode 100644
index 0000000..dd458eb
--- /dev/null
+++ b/vispy/scene/cameras/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+"""
+Cameras are responsible for determining which part of a scene is displayed
+in a viewbox and for handling user input to change the view.
+
+Several Camera subclasses are available to customize the projection of the 
+scene such as 3D perspective and orthographic projections, 2D 
+scale/translation, and other specialty cameras. A variety of user interaction
+styles are available for each camera including arcball, turntable, 
+first-person, and pan/zoom interactions.
+
+Internally, Cameras work by setting the transform of a SubScene object such 
+that a certain part of the scene is mapped to the bounding rectangle of the 
+ViewBox.
+"""
+from .cameras import (make_camera, BaseCamera, PanZoomCamera,  # noqa 
+                      TurntableCamera, FlyCamera, ArcballCamera)  # noqa
+from .magnify import MagnifyCamera, Magnify1DCamera  # noqa
diff --git a/vispy/scene/cameras/cameras.py b/vispy/scene/cameras/cameras.py
new file mode 100644
index 0000000..c808f43
--- /dev/null
+++ b/vispy/scene/cameras/cameras.py
@@ -0,0 +1,1762 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division
+
+import math
+
+import numpy as np
+
+from ...app import Timer
+from ...util.quaternion import Quaternion
+from ...util import keys
+from ..node import Node
+from ...geometry import Rect
+from ...visuals.transforms import (STTransform, PerspectiveTransform,
+                                   NullTransform, AffineTransform,
+                                   TransformCache)
+
+
+# todo: Make 3D cameras use same internal state: less code, smooth transitions
+
+
+def make_camera(cam_type, *args, **kwargs):
+    """ Factory function for creating new cameras using a string name.
+
+    Parameters
+    ----------
+    cam_type : str
+        May be one of:
+
+            * 'panzoom' : Creates :class:`PanZoomCamera`
+            * 'turntable' : Creates :class:`TurntableCamera`
+            * None : Creates :class:`Camera`
+
+    Notes
+    -----
+    All extra arguments are passed to the __init__ method of the selected
+    Camera class.
+    """
+    cam_types = {None: BaseCamera}
+    for camType in (BaseCamera, PanZoomCamera, PerspectiveCamera,
+                    TurntableCamera, FlyCamera, ArcballCamera):
+        cam_types[camType.__name__[:-6].lower()] = camType
+
+    try:
+        return cam_types[cam_type](*args, **kwargs)
+    except KeyError:
+        raise KeyError('Unknown camera type "%s". Options are: %s' %
+                       (cam_type, cam_types.keys()))
+
+
+class BaseCamera(Node):
+    """ Base camera class.
+
+    The Camera describes the perspective from which a ViewBox views its
+    subscene, and the way that user interaction affects that perspective.
+
+    Most functionality is implemented in subclasses. This base class has
+    no user interaction and causes the subscene to use the same coordinate
+    system as the ViewBox.
+
+    Parameters
+    ----------
+    interactive : bool
+        Whether the camera processes mouse and keyboard events.
+    flip : tuple of bools
+        For each dimension, specify whether it is flipped.
+    up : {'+z', '-z', '+y', '-y', '+x', '-x'}
+        The direction that is considered up. Default '+z'. Not all
+        camera's may support this (yet).
+    parent : Node
+        The parent of the camera.
+    name : str
+        Name used to identify the camera in the scene.
+    """
+
+    # These define the state of the camera
+    _state_props = ()
+
+    # The fractional zoom to apply for a single pixel of mouse motion
+    zoom_factor = 0.007
+
+    def __init__(self, interactive=True, flip=None, up='+z', parent=None,
+                 name=None):
+        super(BaseCamera, self).__init__(parent, name)
+
+        # The viewbox for which this camera is active
+        self._viewbox = None
+
+        # Linked cameras
+        self._linked_cameras = []
+        self._linked_cameras_no_update = None
+
+        # Variables related to transforms
+        self.transform = NullTransform()
+        self._pre_transform = None
+        self._viewbox_tr = STTransform()  # correction for viewbox
+        self._projection = PerspectiveTransform()
+        self._transform_cache = TransformCache()
+
+        # For internal use, to store event related information
+        self._event_value = None
+
+        # Flags
+        self._resetting = False  # Avoid lots of updates during set_range
+        self._key_events_bound = False  # Have we connected to key events
+        self._set_range_args = None  # hold set_range() args
+
+        # Limits set in reset (interesting region of the scene)
+        self._xlim = None  # None is flag that no reset has been performed
+        self._ylim = None
+        self._zlim = None
+
+        # Our default state to apply when resetting
+        self._default_state = None
+
+        # We initialize these parameters here, because we want these props
+        # available in all cameras. Note that PanZoom does not use _center
+        self._fov = 0.0
+        self._center = None
+
+        # Set parameters. These are all not part of the "camera state"
+        self.interactive = bool(interactive)
+        self.flip = flip if (flip is not None) else (False, False, False)
+        self.up = up
+
+    def _get_depth_value(self):
+        """ Get the depth value to use in orthographic and perspective projection
+
+        For 24 bits and more, we're fine with 100.000, but for 16 bits we
+        need 3000 or so. The criterion is that at the center, we should be
+        able to distinguish between 0.1, 0.0 and -0.1 etc.
+        """
+        if True:  # bit+depth >= 24
+            return 100000.0
+        else:
+            return 3000.0
+
+    def _depth_to_z(self, depth):
+        """ Get the z-coord, given the depth value.
+        """
+        val = self._get_depth_value()
+        return val - depth * 2 * val
+
+    def _viewbox_set(self, viewbox):
+        """ Friend method of viewbox to register itself.
+        """
+        self._viewbox = viewbox
+        # Connect
+        viewbox.events.mouse_press.connect(self.viewbox_mouse_event)
+        viewbox.events.mouse_release.connect(self.viewbox_mouse_event)
+        viewbox.events.mouse_move.connect(self.viewbox_mouse_event)
+        viewbox.events.mouse_wheel.connect(self.viewbox_mouse_event)
+        viewbox.events.resize.connect(self.viewbox_resize_event)
+        # todo: also add key events! (and also on viewbox (they're missing)
+
+    def _viewbox_unset(self, viewbox):
+        """ Friend method of viewbox to unregister itself.
+        """
+        self._viewbox = None
+        # Disconnect
+        viewbox.events.mouse_press.disconnect(self.viewbox_mouse_event)
+        viewbox.events.mouse_release.disconnect(self.viewbox_mouse_event)
+        viewbox.events.mouse_move.disconnect(self.viewbox_mouse_event)
+        viewbox.events.mouse_wheel.disconnect(self.viewbox_mouse_event)
+        viewbox.events.resize.disconnect(self.viewbox_resize_event)
+
+    @property
+    def viewbox(self):
+        """ The viewbox that this camera applies to.
+        """
+        return self._viewbox
+
+    ## Camera attributes
+
+    @property
+    def interactive(self):
+        """ Boolean describing whether the camera should enable or disable
+        user interaction.
+        """
+        return self._interactive
+
+    @interactive.setter
+    def interactive(self, value):
+        self._interactive = bool(value)
+
+    @property
+    def flip(self):
+        return self._flip
+
+    @flip.setter
+    def flip(self, value):
+        if not isinstance(value, (list, tuple)):
+            raise ValueError('Flip must be a tuple or list.')
+        if len(value) == 2:
+            self._flip = bool(value[0]), bool(value[1]), False
+        elif len(value) == 3:
+            self._flip = bool(value[0]), bool(value[1]), bool(value[2])
+        else:
+            raise ValueError('Flip must have 2 or 3 elements.')
+        self._flip_factors = tuple([(1-x*2) for x in self._flip])
+        self.view_changed()
+
+    @property
+    def up(self):
+        """ The dimension that is considered up.
+        """
+        return self._up
+
+    @up.setter
+    def up(self, value):
+        value = value.lower()
+        value = ('+' + value) if value in 'zyx' else value
+        if value not in ('+z', '-z', '+y', '-y', '+x', '-x'):
+            raise ValueError('Invalid value for up.')
+        self._up = value
+        self.view_changed()
+
+    @property
+    def center(self):
+        """ The center location for this camera
+
+        The exact meaning of this value differs per type of camera, but
+        generally means the point of interest or the rotation point.
+        """
+        return self._center or (0, 0, 0)
+
+    @center.setter
+    def center(self, val):
+        if len(val) == 2:
+            self._center = float(val[0]), float(val[1]), 0.0
+        elif len(val) == 3:
+            self._center = float(val[0]), float(val[1]), float(val[2])
+        else:
+            raise ValueError('Center must be a 2 or 3 element tuple')
+        self.view_changed()
+
+    @property
+    def fov(self):
+        """ Field-of-view angle of the camera. If 0, the camera is in
+        orthographic mode.
+        """
+        return self._fov
+
+    @fov.setter
+    def fov(self, fov):
+        fov = float(fov)
+        if fov < 0 or fov >= 180:
+            raise ValueError("fov must be between 0 and 180.")
+        self._fov = fov
+        self.view_changed()
+
+    ## Camera methods
+
+    def set_range(self, x=None, y=None, z=None, margin=0.05):
+        """ Set the range of the view region for the camera
+
+        Parameters
+        ----------
+        x : tuple | None
+            X range.
+        y : tuple | None
+            Y range.
+        z : tuple | None
+            Z range.
+        margin : float
+            Margin to use.
+
+        Notes
+        -----
+        The view is set to the given range or to the scene boundaries
+        if ranges are not specified. The ranges should be 2-element
+        tuples specifying the min and max for each dimension.
+
+        For the PanZoomCamera the view is fully defined by the range.
+        For e.g. the TurntableCamera the elevation and azimuth are not
+        set. One should use reset() for that.
+        """
+        # Flag to indicate that this is an initializing (not user-invoked)
+        init = self._xlim is None
+
+        # Collect given bounds
+        bounds = [None, None, None]
+        if x is not None:
+            bounds[0] = float(x[0]), float(x[1])
+        if y is not None:
+            bounds[1] = float(y[0]), float(y[1])
+        if z is not None:
+            bounds[2] = float(z[0]), float(z[1])
+        # If there is no viewbox, store given bounds in lim variables, and stop
+        if self._viewbox is None:
+            self._set_range_args = bounds[0], bounds[1], bounds[2], margin
+            return
+
+        # There is a viewbox, we're going to set the range for real
+        self._resetting = True
+
+        # Get bounds from viewbox if not given
+        if all([(b is None) for b in bounds]):
+            bounds = self._viewbox.get_scene_bounds()
+        else:
+            for i in range(3):
+                if bounds[i] is None:
+                    bounds[i] = self._viewbox.get_scene_bounds(i)
+        
+        # Calculate ranges and margins
+        ranges = [b[1] - b[0] for b in bounds]
+        margins = [(r*margin or 0.1) for r in ranges]
+        # Assign limits for this camera
+        bounds_margins = [(b[0]-m, b[1]+m) for b, m in zip(bounds, margins)]
+        self._xlim, self._ylim, self._zlim = bounds_margins
+        # Store center location
+        if (not init) or (self._center is None):
+            self._center = [(b[0] + r / 2) for b, r in zip(bounds, ranges)]
+
+        # Let specific camera handle it
+        self._set_range(init)
+
+        # Finish
+        self._resetting = False
+        self.view_changed()
+
+    def _set_range(self, init):
+        pass
+
+    def reset(self):
+        """ Reset the view to the default state.
+        """
+        self.set_state(self._default_state)
+
+    def set_default_state(self):
+        """ Set the current state to be the default state to be applied
+        when calling reset().
+        """
+        self._default_state = self.get_state()
+
+    def get_state(self):
+        """ Get the current view state of the camera
+
+        Returns a dict of key-value pairs. The exact keys depend on the
+        camera. Can be passed to set_state() (of this or another camera
+        of the same type) to reproduce the state.
+        """
+        D = {}
+        for key in self._state_props:
+            D[key] = getattr(self, key)
+        return D
+
+    def set_state(self, state=None, **kwargs):
+        """ Set the view state of the camera
+
+        Should be a dict (or kwargs) as returned by get_state. It can
+        be an incomlete dict, in which case only the specified
+        properties are set.
+
+        Parameters
+        ----------
+        state : dict
+            The camera state.
+        **kwargs : dict
+            Unused keyword arguments.
+        """
+        D = state or {}
+        D.update(kwargs)
+        for key, val in D.items():
+            if key not in self._state_props:
+                raise KeyError('Not a valid camera state property %r' % key)
+            setattr(self, key, val)
+
+    def link(self, camera):
+        """ Link this camera with another camera of the same type
+
+        Linked camera's keep each-others' state in sync.
+
+        Parameters
+        ----------
+        camera : instance of Camera
+            The other camera to link.
+        """
+        cam1, cam2 = self, camera
+        # Remove if already linked
+        while cam1 in cam2._linked_cameras:
+            cam2._linked_cameras.remove(cam1)
+        while cam2 in cam1._linked_cameras:
+            cam1._linked_cameras.remove(cam2)
+        # Link both ways
+        cam1._linked_cameras.append(cam2)
+        cam2._linked_cameras.append(cam1)
+
+    ## Event-related methods
+
+    def view_changed(self):
+        """ Called when this camera is changes its view. Also called
+        when its associated with a viewbox.
+        """
+        if self._resetting:
+            return  # don't update anything while resetting (are in set_range)
+        if self._viewbox:
+            # Set range if necessary
+            if self._xlim is None:
+                args = self._set_range_args or ()
+                self.set_range(*args)
+            # Store default state if we have not set it yet
+            if self._default_state is None:
+                self.set_default_state()
+            # Do the actual update
+            self._update_transform()
+
+    @property
+    def pre_transform(self):
+        """ A transform to apply to the beginning of the scene transform, in
+        addition to anything else provided by this Camera.
+        """
+        return self._pre_transform
+
+    @pre_transform.setter
+    def pre_transform(self, tr):
+        self._pre_transform = tr
+        self.view_changed()
+
+    def viewbox_mouse_event(self, event):
+        """Viewbox mouse event handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        # todo: connect this method to viewbox.events.key_down
+        # viewbox does not currently support key events
+        # A bit awkward way to connect to our canvas; we need event
+        # object to get a reference to the canvas
+        # todo: fix this, also only receive key events when over the viewbox
+        if not self._key_events_bound:
+            self._key_events_bound = True
+            event.canvas.events.key_press.connect(self.viewbox_key_event)
+            event.canvas.events.key_release.connect(self.viewbox_key_event)
+
+    def viewbox_key_event(self, event):
+        """ViewBox key event handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        if event.key == keys.BACKSPACE:
+            self.reset()
+
+    def viewbox_resize_event(self, event):
+        """The ViewBox resize handler to update the transform
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        pass
+
+    def _update_transform(self):
+        """ Subclasses should reimplement this method to update the scene
+        transform by calling self._set_scene_transform.
+        """
+        self._set_scene_transform(self.transform)
+
+    def _set_scene_transform(self, tr):
+        """ Called by subclasses to configure the viewbox scene transform.
+        """
+        # todo: check whether transform has changed, connect to
+        # transform.changed event
+        pre_tr = self.pre_transform
+        if pre_tr is None:
+            self._scene_transform = tr
+        else:
+            self._transform_cache.roll()
+            self._scene_transform = self._transform_cache.get([pre_tr, tr])
+
+        # Update scene
+        self._viewbox.scene.transform = self._scene_transform
+        self._viewbox.update()
+
+        # Apply same state to linked cameras, but prevent that camera
+        # to return the favor
+        for cam in self._linked_cameras:
+            if cam is self._linked_cameras_no_update:
+                continue
+            try:
+                cam._linked_cameras_no_update = self
+                cam.set_state(self.get_state())
+            finally:
+                cam._linked_cameras_no_update = None
+
+
+class PanZoomCamera(BaseCamera):
+    """ Camera implementing 2D pan/zoom mouse interaction.
+
+    For this camera, the ``scale_factor`` indicates the zoom level, and
+    the ``center`` indicates the center position of the view.
+
+    By default, this camera inverts the y axis of the scene. This usually
+    results in the scene +y axis pointing upward because widgets (including
+    ViewBox) have their +y axis pointing downward.
+
+    Parameters
+    ----------
+    rect : Rect
+        A Rect object or 4-element tuple that specifies the rectangular
+        area to show.
+    aspect : float | None
+        The aspect ratio (i.e. scaling) between x and y dimension of
+        the scene. E.g. to show a square image as square, the aspect
+        should be 1. If None (default) the x and y dimensions are scaled
+        independently.
+    **kwargs : dict
+        Keyword arguments to pass to `BaseCamera`.
+
+    Notes
+    -----
+    Interaction:
+
+        * LMB: pan the view
+        * RMB or scroll: zooms the view
+
+    """
+
+    _state_props = BaseCamera._state_props + ('rect', )
+
+    def __init__(self, rect=None, aspect=None, **kwargs):
+        super(PanZoomCamera, self).__init__(**kwargs)
+
+        self.transform = STTransform()
+
+        # Set camera attributes
+        self.aspect = aspect
+        self._rect = None
+        if rect is not None:
+            self.rect = rect
+
+    @property
+    def aspect(self):
+        """ The ratio between the x and y dimension. E.g. to show a
+        square image as square, the aspect should be 1. If None, the
+        dimensions are scaled automatically, dependening on the
+        available space. Otherwise the ratio between the dimensions
+        is fixed.
+        """
+        return self._aspect
+
+    @aspect.setter
+    def aspect(self, value):
+        if value is None:
+            self._aspect = None
+        else:
+            self._aspect = float(value)
+        self.view_changed()
+
+    def zoom(self, factor, center=None):
+        """ Zoom in (or out) at the given center
+
+        Parameters
+        ----------
+        factor : float or tuple
+            Fraction by which the scene should be zoomed (e.g. a factor of 2
+            causes the scene to appear twice as large).
+        center : tuple of 2-4 elements
+            The center of the view. If not given or None, use the
+            current center.
+        """
+        assert len(center) in (2, 3, 4)
+        
+        # Get scale factor, take scale ratio into account
+        if np.isscalar(factor):
+            scale = [factor, factor]
+        else:
+            if len(factor) != 2:
+                raise TypeError("factor must be scalar or length-2 sequence.")
+            scale = list(factor)
+        if self.aspect is not None:
+            scale[0] = scale[1]
+        
+        # Init some variables
+        center = center if (center is not None) else self.center
+        rect = self.rect
+        # Get space from given center to edges
+        left_space = center[0] - rect.left
+        right_space = rect.right - center[0]
+        bottom_space = center[1] - rect.bottom
+        top_space = rect.top - center[1]
+        # Scale these spaces
+        rect.left = center[0] - left_space * scale[0]
+        rect.right = center[0] + right_space * scale[0]
+        rect.bottom = center[1] - bottom_space * scale[1]
+        rect.top = center[1] + top_space * scale[1]
+
+        self.rect = rect
+
+    def pan(self, *pan):
+        """Pan the view.
+
+        Parameters
+        ----------
+        *pan : length-2 sequence
+            The distance to pan the view, in the coordinate system of the
+            scene.
+        """
+        if len(pan) == 1:
+            pan = pan[0]
+        self.rect = self.rect + pan
+
+    @property
+    def rect(self):
+        """ The rectangular border of the ViewBox visible area, expressed in
+        the coordinate system of the scene.
+
+        Note that the rectangle can have negative width or height, in
+        which case the corresponding dimension is flipped (this flipping
+        is independent from the camera's ``flip`` property).
+        """
+        return self._rect
+
+    @rect.setter
+    def rect(self, args):
+        if isinstance(args, tuple):
+            self._rect = Rect(*args)
+        else:
+            self._rect = Rect(args)
+        self.view_changed()
+
+    @property
+    def center(self):
+        rect = self._rect
+        return 0.5 * (rect.left+rect.right), 0.5 * (rect.top+rect.bottom), 0
+
+    @center.setter
+    def center(self, center):
+        if not (isinstance(center, (tuple, list)) and len(center) in (2, 3)):
+            raise ValueError('center must be a 2 or 3 element tuple')
+        rect = self.rect or Rect(0, 0, 1, 1)
+        # Get half-ranges
+        x2 = 0.5 * (rect.right - rect.left)
+        y2 = 0.5 * (rect.top - rect.bottom)
+        # Apply new ranges
+        rect.left = center[0] - x2
+        rect.right = center[0] + x2
+        rect.bottom = center[1] - y2
+        rect.top = center[1] + y2
+        #
+        self.rect = rect
+
+    def _set_range(self, init):
+        if init and self._rect is not None:
+            return
+        # Convert limits to rect
+        w = self._xlim[1] - self._xlim[0]
+        h = self._ylim[1] - self._ylim[0]
+        self.rect = self._xlim[0], self._ylim[0], w, h
+
+    def viewbox_resize_event(self, event):
+        """ Modify the data aspect and scale factor, to adjust to
+        the new window size.
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        self.view_changed()
+
+    def viewbox_mouse_event(self, event):
+        """
+        The SubScene received a mouse event; update transform
+        accordingly.
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        if event.handled or not self.interactive:
+            return
+
+        # Scrolling
+        BaseCamera.viewbox_mouse_event(self, event)
+
+        if event.type == 'mouse_wheel':
+            center = self._scene_transform.imap(event.pos)
+            self.zoom((1 + self.zoom_factor) ** (-event.delta[1] * 30), center)
+
+        elif event.type == 'mouse_move':
+            if event.press_event is None:
+                return
+
+            modifiers = event.mouse_event.modifiers
+            p1 = event.mouse_event.press_event.pos
+            p2 = event.mouse_event.pos
+
+            if 1 in event.buttons and not modifiers:
+                # Translate
+                p1 = np.array(event.last_event.pos)[:2]
+                p2 = np.array(event.pos)[:2]
+                p1s = self._transform.imap(p1)
+                p2s = self._transform.imap(p2)
+                self.pan(p1s-p2s)
+
+            elif 2 in event.buttons and not modifiers:
+                # Zoom
+                p1c = np.array(event.last_event.pos)[:2]
+                p2c = np.array(event.pos)[:2]
+                scale = ((1 + self.zoom_factor) ** 
+                         ((p1c-p2c) * np.array([1, -1])))
+                center = self._transform.imap(event.press_event.pos[:2])
+                self.zoom(scale, center)
+
+    def _update_transform(self):
+
+        rect = self.rect
+        self._real_rect = Rect(rect)
+        vbr = self._viewbox.rect.flipped(x=self.flip[0], y=(not self.flip[1]))
+        d = self._get_depth_value()
+
+        # apply scale ratio constraint
+        if self._aspect is not None:
+            # Aspect ratio of the requested range
+            requested_aspect = (rect.width / rect.height
+                                if rect.height != 0 else 1)
+            # Aspect ratio of the viewbox
+            view_aspect = vbr.width / vbr.height if vbr.height != 0 else 1
+            # View aspect ratio needed to obey the scale constraint
+            constrained_aspect = abs(view_aspect / self._aspect)
+
+            if requested_aspect > constrained_aspect:
+                # view range needs to be taller than requested
+                dy = 0.5 * (rect.width / constrained_aspect - rect.height)
+                self._real_rect.top += dy
+                self._real_rect.bottom -= dy
+            else:
+                # view range needs to be wider than requested
+                dx = 0.5 * (rect.height * constrained_aspect - rect.width)
+                self._real_rect.left -= dx
+                self._real_rect.right += dx
+
+        # Apply mapping between viewbox and cam view
+        self.transform.set_mapping(self._real_rect, vbr)
+        # Scale z, so that the clipping planes are between -alot and +alot
+        self.transform.zoom((1, 1, 1/d))
+
+        # We've now set self.transform, which represents our 2D
+        # transform When up is +z this is all. In other cases,
+        # self.transform is now set up correctly to allow pan/zoom, but
+        # for the scene we need a different (3D) mapping. When there
+        # is a minus in up, we simply look at the scene from the other
+        # side (as if z was flipped).
+
+        if self.up == '+z':
+            thetransform = self.transform
+        else:
+            rr = self._real_rect
+            tr = AffineTransform()
+            d = d if (self.up[0] == '+') else -d
+            pp1 = [(vbr.left, vbr.bottom, 0), (vbr.left, vbr.top, 0),
+                   (vbr.right, vbr.bottom, 0), (vbr.left, vbr.bottom, 1)]
+            # Get Mapping
+            if self.up[1] == 'z':
+                pp2 = [(rr.left, rr.bottom, 0), (rr.left, rr.top, 0),
+                       (rr.right, rr.bottom, 0), (rr.left, rr.bottom, d)]
+            elif self.up[1] == 'y':
+                pp2 = [(rr.left, 0, rr.bottom), (rr.left, 0, rr.top),
+                       (rr.right, 0, rr.bottom), (rr.left, d, rr.bottom)]
+            elif self.up[1] == 'x':
+                pp2 = [(0, rr.left, rr.bottom), (0, rr.left, rr.top),
+                       (0, rr.right, rr.bottom), (d, rr.left, rr.bottom)]
+            # Apply
+            tr.set_mapping(np.array(pp2), np.array(pp1))
+            thetransform = tr
+
+        # Set on viewbox
+        self._set_scene_transform(thetransform)
+
+
+class PerspectiveCamera(BaseCamera):
+    """ Base class for 3D cameras supporting orthographic and
+    perspective projections.
+
+    Parameters
+    ----------
+    fov : float
+        Field of view. Default 60.0.
+    scale_factor : scalar
+        A measure for the scale/range of the scene that the camera
+        should show. The exact meaning differs per camera type.
+    **kwargs : dict
+        Keyword arguments to pass to `BaseCamera`.
+    """
+
+    _state_props = ('scale_factor', 'center', 'fov')
+
+    def __init__(self, fov=60.0, scale_factor=None, center=None, **kwargs):
+        super(PerspectiveCamera, self).__init__(**kwargs)
+        # Camera transform
+        self.transform = AffineTransform()
+
+        # Set camera attributes
+        self.fov = fov
+        self._scale_factor = None
+        self._center = None
+
+        # Only set if they are given. They're set during _set_range if None
+        if scale_factor is not None:
+            self.scale_factor = scale_factor
+        if center is not None:
+            self.center = center
+
+    def viewbox_mouse_event(self, event):
+        """ The ViewBox received a mouse event; update transform
+        accordingly.
+        Default implementation adjusts scale factor when scolling.
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        BaseCamera.viewbox_mouse_event(self, event)
+        if event.type == 'mouse_wheel':
+            s = 1.1 ** - event.delta[1]
+            self._scale_factor *= s
+            if self._distance is not None:
+                self._distance *= s
+            self.view_changed()
+
+    @property
+    def scale_factor(self):
+        """ The measure for the scale or range that the camera should cover
+
+        For the PanZoomCamera and TurnTableCamera this translates to
+        zooming: set to smaller values to zoom in.
+        """
+        return self._scale_factor
+
+    @scale_factor.setter
+    def scale_factor(self, value):
+        self._scale_factor = abs(float(value))
+        self.view_changed()
+
+    @property
+    def near_clip_distance(self):
+        """ The distance of the near clipping plane from the camera's position.
+        """
+        return self._near_clip_distance
+
+    def _set_range(self, init):
+        """ Reset the camera view using the known limits.
+        """
+
+        if init and (self._scale_factor is not None):
+            return  # We don't have to set our scale factor
+
+        # Get window size (and store factor now to sync with resizing)
+        w, h = self._viewbox.size
+        w, h = float(w), float(h)
+
+        # Get range and translation for x and y
+        x1, y1, z1 = self._xlim[0], self._ylim[0], self._zlim[0]
+        x2, y2, z2 = self._xlim[1], self._ylim[1], self._zlim[1]
+        rx, ry, rz = (x2 - x1), (y2 - y1), (z2 - z1)
+
+        # Correct ranges for window size. Note that the window width
+        # influences the x and y data range, while the height influences
+        # the z data range.
+        if w / h > 1:
+            rx /= w / h
+            ry /= w / h
+        else:
+            rz /= h / w
+
+        # Convert to screen coordinates. In screen x, only x and y have effect.
+        # In screen y, all three dimensions have effect. The idea of the lines
+        # below is to calculate the range on screen when that will fit the
+        # data under any rotation.
+        rxs = (rx**2 + ry**2)**0.5
+        rys = (rx**2 + ry**2 + rz**2)**0.5
+
+        self.scale_factor = max(rxs, rys) * 1.04  # 4% extra space
+
+    def viewbox_resize_event(self, event):
+        """The ViewBox resize handler to update the transform
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        self.view_changed()
+
+    def _update_transform(self, event=None):
+        # Do we have a viewbox
+        if self._viewbox is None:
+            return
+
+        # Calculate viewing range for x and y
+        fx = fy = self._scale_factor
+
+        # Correct for window size
+        w, h = self._viewbox.size
+        if w / h > 1:
+            fx *= w / h
+        else:
+            fy *= h / w
+
+        self._update_projection_transform(fx, fy)
+
+        # assemble complete transform mapping to viewbox bounds
+        unit = [[-1, 1], [1, -1]]
+        vrect = [[0, 0], self._viewbox.size]
+        self._viewbox_tr.set_mapping(unit, vrect)
+        transforms = [n.transform for n in
+                      self._viewbox.scene.node_path_to_child(self)[1:]]
+        camera_tr = self._transform_cache.get(transforms).inverse
+        full_tr = self._transform_cache.get([self._viewbox_tr,
+                                             self._projection,
+                                             camera_tr])
+        self._transform_cache.roll()
+        self._set_scene_transform(full_tr)
+
+    def _update_projection_transform(self, fx, fy):
+        d = self._get_depth_value()
+        if self._fov == 0:
+            self._projection.set_ortho(-0.5*fx, 0.5*fx, -0.5*fy, 0.5*fy, 0, d)
+        else:
+            fov = max(0.01, self._fov)
+            dist = fy / (2 * math.tan(math.radians(fov)/2))
+            val = math.sqrt(d)
+            self._projection.set_perspective(fov, fx/fy, dist/val, dist*val)
+
+
+class Base3DRotationCamera(PerspectiveCamera):
+    """Base class for TurntableCamera and ArcballCamera"""
+
+    def __init__(self, fov=0.0, **kwargs):
+        super(Base3DRotationCamera, self).__init__(fov=fov, **kwargs)
+        self._actual_distance = 0.0
+        self._event_value = None
+
+    @property
+    def distance(self):
+        """ The user-set distance. If None (default), the distance is
+        internally calculated from the scale factor and fov.
+        """
+        return self._distance
+
+    @distance.setter
+    def distance(self, distance):
+        if distance is None:
+            self._distance = None
+        else:
+            self._distance = float(distance)
+        self.view_changed()
+
+    def viewbox_mouse_event(self, event):
+        """
+        The viewbox received a mouse event; update transform
+        accordingly.
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        if event.handled or not self.interactive:
+            return
+
+        PerspectiveCamera.viewbox_mouse_event(self, event)
+
+        if event.type == 'mouse_release':
+            self._event_value = None  # Reset
+
+        elif event.type == 'mouse_move':
+            if event.press_event is None:
+                return
+
+            modifiers = event.mouse_event.modifiers
+            p1 = event.mouse_event.press_event.pos
+            p2 = event.mouse_event.pos
+            d = p2 - p1
+
+            if 1 in event.buttons and not modifiers:
+                # Rotate
+                self._update_rotation(event)
+
+            elif 2 in event.buttons and not modifiers:
+                # Zoom
+                if self._event_value is None:
+                    self._event_value = (self._scale_factor, self._distance)
+                zoomy = (1 + self.zoom_factor) ** d[1]
+                
+                self.scale_factor = self._event_value[0] * zoomy
+                # Modify distance if its given
+                if self._distance is not None:
+                    self._distance = self._event_value[1] * zoomy
+                self.view_changed()
+
+            elif 1 in event.buttons and keys.SHIFT in modifiers:
+                # Translate
+                norm = np.mean(self._viewbox.size)
+                if self._event_value is None:
+                    self._event_value = self.center
+                dist = (p1 - p2) / norm * self._scale_factor
+                dist[1] *= -1
+                # Black magic part 1: turn 2D into 3D translations
+                dx, dy, dz = self._dist_to_trans(dist)
+                # Black magic part 2: take up-vector and flipping into account
+                ff = self._flip_factors
+                up, forward, right = self._get_dim_vectors()
+                dx, dy, dz = right * dx + forward * dy + up * dz
+                dx, dy, dz = ff[0] * dx, ff[1] * dy, dz * ff[2]
+                c = self._event_value
+                self.center = c[0] + dx, c[1] + dy, c[2] + dz
+
+            elif 2 in event.buttons and keys.SHIFT in modifiers:
+                # Change fov
+                if self._event_value is None:
+                    self._event_value = self._fov
+                fov = self._event_value - d[1] / 5.0
+                self.fov = min(180.0, max(0.0, fov))
+
+    def _update_camera_pos(self):
+        """ Set the camera position and orientation"""
+
+        # transform will be updated several times; do not update camera
+        # transform until we are done.
+        ch_em = self.events.transform_change
+        with ch_em.blocker(self._update_transform):
+            tr = self.transform
+            tr.reset()
+
+            up, forward, right = self._get_dim_vectors()
+
+            # Create mapping so correct dim is up
+            pp1 = np.array([(0, 0, 0), (0, 0, -1), (1, 0, 0), (0, 1, 0)])
+            pp2 = np.array([(0, 0, 0), forward, right, up])
+            tr.set_mapping(pp1, pp2)
+
+            tr.translate(-self._actual_distance * forward)
+            self._rotate_tr()
+            tr.scale([1.0/a for a in self._flip_factors])
+            tr.translate(np.array(self.center))
+
+    def _get_dim_vectors(self):
+        # Specify up and forward vector
+        M = {'+z': [(0, 0, +1), (0, 1, 0)],
+             '-z': [(0, 0, -1), (0, 1, 0)],
+             '+y': [(0, +1, 0), (1, 0, 0)],
+             '-y': [(0, -1, 0), (1, 0, 0)],
+             '+x': [(+1, 0, 0), (0, 0, 1)],
+             '-x': [(-1, 0, 0), (0, 0, 1)],
+             }
+        up, forward = M[self.up]
+        right = np.cross(forward, up)
+        return np.array(up), np.array(forward), right
+
+    def _update_projection_transform(self, fx, fy):
+        d = self._get_depth_value()
+        if self._fov == 0:
+            self._projection.set_ortho(-0.5*fx, 0.5*fx, -0.5*fy, 0.5*fy, -d, d)
+            self._actual_distance = self._distance or 0.0
+        else:
+            # Figure distance to center in order to have correct FoV and fy.
+            # Use that auto-distance, or the given distance (if not None).
+            fov = max(0.01, self._fov)
+            dist = fy / (2 * math.tan(math.radians(fov)/2))
+            self._actual_distance = dist = self._distance or dist
+            val = math.sqrt(d*10)
+            self._projection.set_perspective(fov, fx/fy, dist/val, dist*val)
+        # Update camera pos, which will use our calculated _distance to offset
+        # the camera
+        self._update_camera_pos()
+
+    def _update_rotation(self, event):
+        """Update rotation parmeters based on mouse movement"""
+        raise NotImplementedError
+
+    def _rotate_tr(self):
+        """Rotate the transformation matrix based on camera parameters"""
+        raise NotImplementedError
+
+    def _dist_to_trans(self, dist):
+        """Convert mouse x, y movement into x, y, z translations"""
+        raise NotImplementedError
+
+
+class TurntableCamera(Base3DRotationCamera):
+    """ 3D camera class that orbits around a center point while
+    maintaining a view on a center point.
+
+    For this camera, the ``scale_factor`` indicates the zoom level, and
+    the ``center`` indicates the position to put at the center of the
+    view.
+
+    Parameters
+    ----------
+    fov : float
+        Field of view. Zero (default) means orthographic projection.
+    elevation : float
+        Elevation angle in degrees. Positive angles place the camera
+        above the cente point, negative angles place the camera below
+        the center point.
+    azimuth : float
+        Azimuth angle in degrees. Zero degrees places the camera on the
+        positive x-axis, pointing in the negative x direction.
+    roll : float
+        Roll angle in degrees
+    distance : float | None
+        The distance of the camera from the rotation point (only makes sense
+        if fov > 0). If None (default) the distance is determined from the
+        scale_factor and fov.
+    **kwargs : dict
+        Keyword arguments to pass to `BaseCamera`.
+
+    Notes
+    -----
+    Interaction:
+
+        * LMB: orbits the view around its center point.
+        * RMB or scroll: change scale_factor (i.e. zoom level)
+        * SHIFT + LMB: translate the center point
+        * SHIFT + RMB: change FOV
+
+    """
+
+    _state_props = Base3DRotationCamera._state_props + ('elevation',
+                                                        'azimuth', 'roll')
+
+    def __init__(self, fov=0.0, elevation=30.0, azimuth=30.0, roll=0.0,
+                 distance=None, **kwargs):
+        super(TurntableCamera, self).__init__(fov=fov, **kwargs)
+
+        # Set camera attributes
+        self.azimuth = azimuth
+        self.elevation = elevation
+        self.roll = roll  # interaction not implemented yet
+        self.distance = distance  # None means auto-distance
+
+    @property
+    def elevation(self):
+        """ The angle of the camera in degrees above the horizontal (x, z)
+        plane.
+        """
+        return self._elevation
+
+    @elevation.setter
+    def elevation(self, elev):
+        elev = float(elev)
+        self._elevation = min(90, max(-90, elev))
+        self.view_changed()
+
+    @property
+    def azimuth(self):
+        """ The angle of the camera in degrees around the y axis. An angle of
+        0 places the camera within the (y, z) plane.
+        """
+        return self._azimuth
+
+    @azimuth.setter
+    def azimuth(self, azim):
+        azim = float(azim)
+        while azim < -180:
+            azim += 360
+        while azim > 180:
+            azim -= 360
+        self._azimuth = azim
+        self.view_changed()
+
+    @property
+    def roll(self):
+        """ The angle of the camera in degrees around the z axis. An angle of
+        0 places puts the camera upright.
+        """
+        return self._roll
+
+    @roll.setter
+    def roll(self, roll):
+        roll = float(roll)
+        while roll < -180:
+            roll += 360
+        while roll > 180:
+            roll -= 360
+        self._roll = roll
+        self.view_changed()
+
+    def orbit(self, azim, elev):
+        """ Orbits the camera around the center position.
+
+        Parameters
+        ----------
+        azim : float
+            Angle in degrees to rotate horizontally around the center point.
+        elev : float
+            Angle in degrees to rotate vertically around the center point.
+        """
+        self.azimuth += azim
+        self.elevation = np.clip(self.elevation + elev, -90, 90)
+        self.view_changed()
+
+    def _update_rotation(self, event):
+        """Update rotation parmeters based on mouse movement"""
+        p1 = event.mouse_event.press_event.pos
+        p2 = event.mouse_event.pos
+        if self._event_value is None:
+            self._event_value = self.azimuth, self.elevation
+        self.azimuth = self._event_value[0] - (p2 - p1)[0] * 0.5
+        self.elevation = self._event_value[1] + (p2 - p1)[1] * 0.5
+
+    def _rotate_tr(self):
+        """Rotate the transformation matrix based on camera parameters"""
+        up, forward, right = self._get_dim_vectors()
+        self.transform.rotate(self.elevation, -right)
+        self.transform.rotate(self.azimuth, up)
+
+    def _dist_to_trans(self, dist):
+        """Convert mouse x, y movement into x, y, z translations"""
+        rae = np.array([self.roll, self.azimuth, self.elevation]) * np.pi / 180
+        sro, saz, sel = np.sin(rae)
+        cro, caz, cel = np.cos(rae)
+        dx = (+ dist[0] * (cro * caz + sro * sel * saz)
+              + dist[1] * (sro * caz - cro * sel * saz))
+        dy = (+ dist[0] * (cro * saz - sro * sel * caz)
+              + dist[1] * (sro * saz + cro * sel * caz))
+        dz = (- dist[0] * sro * cel + dist[1] * cro * cel)
+        return dx, dy, dz
+
+
+class ArcballCamera(Base3DRotationCamera):
+    """ 3D camera class that orbits around a center point while
+    maintaining a view on a center point.
+
+    For this camera, the ``scale_factor`` indicates the zoom level, and
+    the ``center`` indicates the position to put at the center of the
+    view.
+
+    Parameters
+    ----------
+    fov : float
+        Field of view. Zero (default) means orthographic projection.
+    distance : float | None
+        The distance of the camera from the rotation point (only makes sense
+        if fov > 0). If None (default) the distance is determined from the
+        scale_factor and fov.
+    **kwargs : dict
+        Keyword arguments to pass to `BaseCamera`.
+
+    Notes
+    -----
+    Interaction:
+
+        * LMB: orbits the view around its center point.
+        * RMB or scroll: change scale_factor (i.e. zoom level)
+        * SHIFT + LMB: translate the center point
+        * SHIFT + RMB: change FOV
+
+    """
+
+    _state_props = Base3DRotationCamera._state_props
+
+    def __init__(self, fov=0.0, distance=None, **kwargs):
+        super(ArcballCamera, self).__init__(fov=fov, **kwargs)
+
+        # Set camera attributes
+        self._quaternion = Quaternion()
+        self.distance = distance  # None means auto-distance
+
+    def _update_rotation(self, event):
+        """Update rotation parmeters based on mouse movement"""
+        p2 = event.mouse_event.pos
+        if self._event_value is None:
+            self._event_value = p2
+        wh = self._viewbox.size
+        self._quaternion = (Quaternion(*_arcball(p2, wh)) *
+                            Quaternion(*_arcball(self._event_value, wh)) *
+                            self._quaternion)
+        self._event_value = p2
+        self.view_changed()
+
+    def _rotate_tr(self):
+        """Rotate the transformation matrix based on camera parameters"""
+        rot, x, y, z = self._quaternion.get_axis_angle()
+        up, forward, right = self._get_dim_vectors()
+        self.transform.rotate(180 * rot / np.pi, (x, z, y))
+
+    def _dist_to_trans(self, dist):
+        """Convert mouse x, y movement into x, y, z translations"""
+        rot, x, y, z = self._quaternion.get_axis_angle()
+        tr = AffineTransform()
+        tr.rotate(180 * rot / np.pi, (x, y, z))
+        dx, dz, dy = np.dot(tr.matrix[:3, :3], (dist[0], dist[1], 0.))
+        return dx, dy, dz
+
+    def _get_dim_vectors(self):
+        # Override vectors, camera has no sense of "up"
+        return np.eye(3)[::-1]
+
+
+def _arcball(xy, wh):
+    """Convert x,y coordinates to w,x,y,z Quaternion parameters
+
+    Adapted from:
+
+    linalg library
+
+    Copyright (c) 2010-2015, Renaud Blanch <rndblnch at gmail dot com>
+    Licence at your convenience:
+    GPLv3 or higher <http://www.gnu.org/licenses/gpl.html>
+    BSD new <http://opensource.org/licenses/BSD-3-Clause>
+    """
+    x, y = xy
+    w, h = wh
+    r = (w + h) / 2.
+    x, y = -(2. * x - w) / r, (2. * y - h) / r
+    h = np.sqrt(x*x + y*y)
+    return (0., x/h, y/h, 0.) if h > 1. else (0., x, y, np.sqrt(1. - h*h))
+
+
+class FlyCamera(PerspectiveCamera):
+    """ The fly camera provides a way to explore 3D data using an
+    interaction style that resembles a flight simulator.
+
+    For this camera, the ``scale_factor`` indicates the speed of the
+    camera in units per second, and the ``center`` indicates the
+    position of the camera.
+
+    Parameters
+    ----------
+    fov : float
+        Field of view. Default 60.0.
+    rotation : float | None
+        Rotation to use.
+    **kwargs : dict
+        Keyword arguments to pass to `BaseCamera`.
+
+    Notes
+    -----
+    Interacting with this camera might need a bit of practice.
+    The reaction to key presses can be customized by modifying the
+    keymap property.
+
+    Moving:
+
+      * arrow keys, or WASD to move forward, backward, left and right
+      * F and C keys move up and down
+      * Space bar to brake
+
+    Viewing:
+
+      * Use the mouse while holding down LMB to control the pitch and yaw.
+      * Alternatively, the pitch and yaw can be changed using the keys
+        IKJL
+      * The camera auto-rotates to make the bottom point down, manual
+        rolling can be performed using Q and E.
+
+    """
+
+    # Linking this camera likely not to work very well
+    _state_props = PerspectiveCamera._state_props + ('rotation', )
+
+    def __init__(self, fov=60, rotation=None, **kwargs):
+
+        # Motion speed vector
+        self._speed = np.zeros((6,), 'float64')
+
+        # Acceleration and braking vectors, set from keyboard
+        self._brake = np.zeros((6,), 'uint8')  # bool-ish
+        self._acc = np.zeros((6,), 'float64')
+
+        # Init rotations
+        self._auto_roll = True  # Whether to roll to make Z up
+        self._rotation1 = Quaternion()  # The base rotation
+        self._rotation2 = Quaternion()  # The delta yaw and pitch rotation
+
+        PerspectiveCamera.__init__(self, fov=fov, **kwargs)
+
+        # Set camera attributes
+        self.rotation = rotation if (rotation is not None) else Quaternion()
+
+        # To store data at start of interaction
+        self._event_value = None
+
+        # Whether the mouse-system wants a transform update
+        self._update_from_mouse = False
+
+        # Mapping that defines keys to thrusters
+        self._keymap = {
+            keys.UP: (+1, 1), keys.DOWN: (-1, 1),
+            keys.RIGHT: (+1, 2), keys.LEFT: (-1, 2),
+            #
+            'W': (+1, 1), 'S': (-1, 1),
+            'D': (+1, 2), 'A': (-1, 2),
+            'F': (+1, 3), 'C': (-1, 3),
+            #
+            'I': (+1, 4), 'K': (-1, 4),
+            'L': (+1, 5), 'J': (-1, 5),
+            'Q': (+1, 6), 'E': (-1, 6),
+            #
+            keys.SPACE: (0, 1, 2, 3),  # 0 means brake, apply to translation
+            #keys.ALT: (+5, 1),  # Turbo
+        }
+
+        # Timer. Each tick we calculate new speed and new position
+        self._timer = Timer(0.01, start=False, connect=self.on_timer)
+
+    @property
+    def rotation(self):
+        """ Get the full rotation. This rotation is composed of the
+        normal rotation plus the extra rotation due to the current
+        interaction of the user.
+        """
+        rotation = self._rotation2 * self._rotation1
+        return rotation.normalize()
+
+    @rotation.setter
+    def rotation(self, value):
+        assert isinstance(value, Quaternion)
+        self._rotation1 = value
+
+    @property
+    def auto_roll(self):
+        """ Whether to rotate the camera automaticall to try and attempt
+        to keep Z up.
+        """
+        return self._auto_roll
+
+    @auto_roll.setter
+    def auto_roll(self, value):
+        self._auto_roll = bool(value)
+
+    @property
+    def keymap(self):
+        """ A dictionary that maps keys to thruster directions
+
+        The keys in this dictionary are vispy key descriptions (from
+        vispy.keys) or characters that represent keys. These are matched
+        to the "key" attribute of key-press and key-release events.
+
+        The values are tuples, in which the first element specifies the
+        magnitude of the acceleration, using negative values for
+        "backward" thrust. A value of zero means to brake. The remaining
+        elements specify the dimension to which the acceleration should
+        be applied. These are 1, 2, 3 for forward/backward, left/right,
+        up/down, and 4, 5, 6 for pitch, yaw, roll.
+        """
+        return self._keymap
+
+    def _set_range(self, init):
+        """ Reset the view.
+        """
+
+        #PerspectiveCamera._set_range(self, init)
+
+        # Stop moving
+        self._speed *= 0.0
+
+        # Get window size (and store factor now to sync with resizing)
+        w, h = self._viewbox.size
+        w, h = float(w), float(h)
+
+        # Get range and translation for x and y
+        x1, y1, z1 = self._xlim[0], self._ylim[0], self._zlim[0]
+        x2, y2, z2 = self._xlim[1], self._ylim[1], self._zlim[1]
+        rx, ry, rz = (x2 - x1), (y2 - y1), (z2 - z1)
+
+        # Correct ranges for window size. Note that the window width
+        # influences the x and y data range, while the height influences
+        # the z data range.
+        if w / h > 1:
+            rx /= w / h
+            ry /= w / h
+        else:
+            rz /= h / w
+
+        # Do not convert to screen coordinates. This camera does not need
+        # to fit everything on screen, but we need to estimate the scale
+        # of the data in the scene.
+
+        # Set scale, depending on data range. Initial speed is such that
+        # the scene can be traversed in about three seconds.
+        self._scale_factor = max(rx, ry, rz) / 3.0
+
+        # Set initial position to a corner of the scene
+        margin = np.mean([rx, ry, rz]) * 0.1
+        self._center = x1 - margin, y1 - margin, z1 + margin
+
+        # Determine initial view direction based on flip axis
+        yaw = 45 * self._flip_factors[0]
+        pitch = -90 - 20 * self._flip_factors[2]
+        if self._flip_factors[1] < 0:
+            yaw += 90 * np.sign(self._flip_factors[0])
+
+        # Set orientation
+        q1 = Quaternion.create_from_axis_angle(pitch*math.pi/180, 1, 0, 0)
+        q2 = Quaternion.create_from_axis_angle(0*math.pi/180, 0, 1, 0)
+        q3 = Quaternion.create_from_axis_angle(yaw*math.pi/180, 0, 0, 1)
+        #
+        self._rotation1 = (q1 * q2 * q3).normalize()
+        self._rotation2 = Quaternion()
+
+        # Update
+        self.view_changed()
+
+    def _get_directions(self):
+
+        # Get reference points in reference coordinates
+        #p0 = Point(0,0,0)
+        pf = (0, 0, -1)  # front
+        pr = (1, 0, 0)  # right
+        pl = (-1, 0, 0)  # left
+        pu = (0, 1, 0)  # up
+
+        # Get total rotation
+        rotation = self.rotation.inverse()
+
+        # Transform to real coordinates
+        pf = rotation.rotate_point(pf)
+        pr = rotation.rotate_point(pr)
+        pl = rotation.rotate_point(pl)
+        pu = rotation.rotate_point(pu)
+
+        def _normalize(p):
+            L = sum(x**2 for x in p) ** 0.5
+            return np.array(p, 'float64') / L
+
+        pf = _normalize(pf)
+        pr = _normalize(pr)
+        pl = _normalize(pl)
+        pu = _normalize(pu)
+
+        return pf, pr, pl, pu
+
+    def on_timer(self, event):
+        """Timer event handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+
+        # Set relative speed and acceleration
+        rel_speed = event.dt
+        rel_acc = 0.1
+
+        # Get what's forward
+        pf, pr, pl, pu = self._get_directions()
+
+        # Increase speed through acceleration
+        # Note that self._speed is relative. We can balance rel_acc and
+        # rel_speed to get a nice smooth or direct control
+        self._speed += self._acc * rel_acc
+
+        # Reduce speed. Simulate resistance. Using brakes slows down faster.
+        # Note that the way that we reduce speed, allows for higher
+        # speeds if keys ar bound to higher acc values (i.e. turbo)
+        reduce = np.array([0.05, 0.05, 0.05, 0.1, 0.1, 0.1])
+        reduce[self._brake > 0] = 0.2
+        self._speed -= self._speed * reduce
+        if np.abs(self._speed).max() < 0.05:
+            self._speed *= 0.0
+
+        # --- Determine new position from translation speed
+
+        if self._speed[:3].any():
+
+            # Create speed vectors, use scale_factor as a reference
+            dv = np.array([1.0/d for d in self._flip_factors])
+            #
+            vf = pf * dv * rel_speed * self._scale_factor
+            vr = pr * dv * rel_speed * self._scale_factor
+            vu = pu * dv * rel_speed * self._scale_factor
+            direction = vf, vr, vu
+
+            # Set position
+            center_loc = np.array(self._center, dtype='float32')
+            center_loc += (self._speed[0] * direction[0] +
+                           self._speed[1] * direction[1] +
+                           self._speed[2] * direction[2])
+            self._center = tuple(center_loc)
+
+        # --- Determine new orientation from rotation speed
+
+        roll_angle = 0
+
+        # Calculate manual roll (from speed)
+        if self._speed[3:].any():
+            angleGain = np.array([1.0, 1.5, 1.0]) * 3 * math.pi / 180
+            angles = self._speed[3:] * angleGain
+
+            q1 = Quaternion.create_from_axis_angle(angles[0], -1, 0, 0)
+            q2 = Quaternion.create_from_axis_angle(angles[1], 0, 1, 0)
+            q3 = Quaternion.create_from_axis_angle(angles[2], 0, 0, -1)
+            q = q1 * q2 * q3
+            self._rotation1 = (q * self._rotation1).normalize()
+
+        # Calculate auto-roll
+        if self.auto_roll:
+            up = {'x': (1, 0, 0), 'y': (0, 1, 0), 'z': (0, 0, 1)}[self.up[1]]
+            up = np.array(up) * {'+': +1, '-': -1}[self.up[0]]
+
+            def angle(p1, p2):
+                return np.arccos(p1.dot(p2))
+            #au = angle(pu, (0, 0, 1))
+            ar = angle(pr, up)
+            al = angle(pl, up)
+            af = angle(pf, up)
+            # Roll angle that's off from being leveled (in unit strength)
+            roll_angle = math.sin(0.5*(al - ar))
+            # Correct for pitch
+            roll_angle *= abs(math.sin(af))  # abs(math.sin(au))
+            if abs(roll_angle) < 0.05:
+                roll_angle = 0
+            if roll_angle:
+                # Correct to soften the force at 90 degree angle
+                roll_angle = np.sign(roll_angle) * np.abs(roll_angle)**0.5
+                # Get correction for this iteration and apply
+                angle_correction = 1.0 * roll_angle * math.pi / 180
+                q = Quaternion.create_from_axis_angle(angle_correction,
+                                                      0, 0, 1)
+                self._rotation1 = (q * self._rotation1).normalize()
+
+        # Update
+        if self._speed.any() or roll_angle or self._update_from_mouse:
+            self._update_from_mouse = False
+            self.view_changed()
+
+    def viewbox_key_event(self, event):
+        """ViewBox key event handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        PerspectiveCamera.viewbox_key_event(self, event)
+
+        if event.handled or not self.interactive:
+            return
+
+        # Ensure the timer runs
+        if not self._timer.running:
+            self._timer.start()
+
+        if event.key in self._keymap:
+            val_dims = self._keymap[event.key]
+            val = val_dims[0]
+            # Brake or accelarate?
+            if val == 0:
+                vec = self._brake
+                val = 1
+            else:
+                vec = self._acc
+            # Set
+            if event.type == 'key_release':
+                val = 0
+            for dim in val_dims[1:]:
+                factor = 1.0
+                vec[dim-1] = val * factor
+
+    def viewbox_mouse_event(self, event):
+        """ViewBox mouse event handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        PerspectiveCamera.viewbox_mouse_event(self, event)
+
+        if event.handled or not self.interactive:
+            return
+
+        if event.type == 'mouse_wheel':
+            if not event.mouse_event.modifiers:
+                # Move forward / backward
+                self._speed[0] += 0.5 * event.delta[1]
+            elif keys.SHIFT in event.mouse_event.modifiers:
+                # Speed
+                s = 1.1 ** - event.delta[1]
+                self.scale_factor /= s  # divide instead of multiply
+                print('scale factor: %1.1f units/s' % self.scale_factor)
+            return
+
+        if event.type == 'mouse_release':
+            # Reset
+            self._event_value = None
+            # Apply rotation
+            self._rotation1 = (self._rotation2 * self._rotation1).normalize()
+            self._rotation2 = Quaternion()
+        elif not self._timer.running:
+            # Ensure the timer runs
+            self._timer.start()
+
+        if event.type == 'mouse_move':
+
+            if event.press_event is None:
+                return
+            if not event.buttons:
+                return
+
+            # Prepare
+            modifiers = event.mouse_event.modifiers
+            pos1 = event.mouse_event.press_event.pos
+            pos2 = event.mouse_event.pos
+            w, h = self._viewbox.size
+
+            if 1 in event.buttons and not modifiers:
+                # rotate
+
+                # get normalized delta values
+                d_az = -float(pos2[0] - pos1[0]) / w
+                d_el = +float(pos2[1] - pos1[1]) / h
+                # Apply gain
+                d_az *= - 0.5 * math.pi  # * self._speed_rot
+                d_el *= + 0.5 * math.pi  # * self._speed_rot
+                # Create temporary quaternions
+                q_az = Quaternion.create_from_axis_angle(d_az, 0, 1, 0)
+                q_el = Quaternion.create_from_axis_angle(d_el, 1, 0, 0)
+
+                # Apply to global quaternion
+                self._rotation2 = (q_el.normalize() * q_az).normalize()
+
+            elif 2 in event.buttons and keys.CONTROL in modifiers:
+                # zoom --> fov
+                if self._event_value is None:
+                    self._event_value = self._fov
+                p1 = np.array(event.press_event.pos)[:2]
+                p2 = np.array(event.pos)[:2]
+                p1c = event.map_to_canvas(p1)[:2]
+                p2c = event.map_to_canvas(p2)[:2]
+                d = p2c - p1c
+                fov = self._event_value * math.exp(-0.01*d[1])
+                self._fov = min(90.0, max(10, fov))
+
+        # Make transform be updated on the next timer tick.
+        # By doing it at timer tick, we avoid shaky behavior
+        self._update_from_mouse = True
+
+    def _update_projection_transform(self, fx, fy):
+        PerspectiveCamera._update_projection_transform(self, fx, fy)
+
+        # Turn our internal quaternion representation into rotation
+        # of our transform
+
+        axis_angle = self.rotation.get_axis_angle()
+        angle = axis_angle[0] * 180 / math.pi
+
+        tr = self.transform
+        tr.reset()
+        #
+        tr.rotate(-angle, axis_angle[1:])
+        tr.scale([1.0/a for a in self._flip_factors])
+        tr.translate(self._center)
+
+
+#class FirstPersonCamera(PerspectiveCamera):
+#    pass
diff --git a/vispy/scene/cameras/magnify.py b/vispy/scene/cameras/magnify.py
new file mode 100644
index 0000000..ec1f84a
--- /dev/null
+++ b/vispy/scene/cameras/magnify.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+from __future__ import division
+
+import numpy as np
+
+from .cameras import PanZoomCamera
+from ...visuals.transforms.nonlinear import (MagnifyTransform, 
+                                             Magnify1DTransform)
+from ...app import Timer
+
+
+class MagnifyCamera(PanZoomCamera):
+    """Camera implementing a MagnifyTransform combined with PanZoomCamera.
+    
+    Parameters
+    ----------
+    size_factor : float
+        The size factor to use.
+    radius_ratio : float
+        The radius ratio to use.
+    **kwargs : dict
+        Keyword arguments to pass to `PanZoomCamera` and create a transform.
+
+    Notes
+    -----
+    This Camera uses the mouse cursor position to set the center position of
+    the MagnifyTransform, and uses mouse wheel events to adjust the 
+    magnification factor.
+    
+    At high magnification, very small mouse movements can result in large
+    changes, so we use a timer to animate transitions in the transform 
+    properties.
+    
+    The camera also adjusts the size of its "lens" area when the view is
+    resized.
+
+    """
+    transform_class = MagnifyTransform
+    
+    def __init__(self, size_factor=0.25, radius_ratio=0.9, **kwargs):
+        # what fraction of the view width to use for radius
+        self.size_factor = size_factor
+        
+        # ratio of inner to outer lens radius
+        self.radius_ratio = radius_ratio
+        
+        # Extract kwargs for panzoom
+        camkwargs = {}
+        for key in ('parent', 'name', 'rect', 'aspect'):
+            if key in kwargs:
+                camkwargs[key] = kwargs.pop(key)
+        
+        # Create the mag transform - kwrds go here
+        self.mag = self.transform_class(**kwargs)
+        
+        # for handling smooth transitions
+        self.mag_target = self.mag.mag
+        self.mag._mag = self.mag_target
+        self.mouse_pos = None
+        self.timer = Timer(interval=0.016, connect=self.on_timer)
+        
+        super(MagnifyCamera, self).__init__(**camkwargs)
+
+        # This tells the camera to insert the magnification transform at the
+        # beginning of the transform it applies to the scene. This is the 
+        # correct place for the mag transform because:
+        # 1. We want it to apply to everything inside the scene, and not to
+        #    the ViewBox itself or anything outside of the ViewBox.
+        # 2. We do _not_ want the pan/zoom transforms applied first, because
+        #    the scale factors implemented there should not change the shape
+        #    of the lens.
+        self.pre_transform = self.mag
+    
+    def _viewbox_set(self, viewbox):
+        PanZoomCamera._viewbox_set(self, viewbox)
+        
+    def _viewbox_unset(self, viewbox):
+        PanZoomCamera._viewbox_unset(self, viewbox)
+        self.timer.stop()
+    
+    def viewbox_mouse_event(self, event):
+        """ViewBox mouse event handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The mouse event.
+        """
+        # When the attached ViewBox reseives a mouse event, it is sent to the
+        # camera here.
+        
+        self.mouse_pos = event.pos[:2]
+        if event.type == 'mouse_wheel':
+            # wheel rolled; adjust the magnification factor and hide the 
+            # event from the superclass
+            m = self.mag_target 
+            m *= 1.2 ** event.delta[1]
+            m = m if m > 1 else 1
+            self.mag_target = m
+        else:
+            # send everything _except_ wheel events to the superclass
+            super(MagnifyCamera, self).viewbox_mouse_event(event)
+            
+        # start the timer to smoothly modify the transform properties. 
+        if not self.timer.running:
+            self.timer.start()
+        
+        self._update_transform()
+    
+    def on_timer(self, event=None):
+        """Timer event handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The timer event.
+        """
+        # Smoothly update center and magnification properties of the transform
+        k = np.clip(100. / self.mag.mag, 10, 100)
+        s = 10**(-k * event.dt)
+            
+        c = np.array(self.mag.center)
+        c1 = c * s + self.mouse_pos * (1-s)
+        
+        m = self.mag.mag * s + self.mag_target * (1-s)
+        
+        # If changes are very small, then it is safe to stop the timer.
+        if (np.all(np.abs((c - c1) / c1) < 1e-5) and 
+                (np.abs(np.log(m / self.mag.mag)) < 1e-3)):
+            self.timer.stop()
+            
+        self.mag.center = c1
+        self.mag.mag = m
+            
+        self._update_transform()
+
+    def viewbox_resize_event(self, event):
+        """ViewBox resize event handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The viewbox resize event.
+        """
+        PanZoomCamera.viewbox_resize_event(self, event)
+        self.view_changed()
+
+    def view_changed(self):
+        # make sure radii are updated when a view is attached.
+        # when the view resizes, we change the lens radii to match.
+        if self._viewbox is not None:
+            vbs = self._viewbox.size
+            r = min(vbs) * self.size_factor
+            self.mag.radii = r * self.radius_ratio, r
+
+        PanZoomCamera.view_changed(self)
+
+
+class Magnify1DCamera(MagnifyCamera):
+    transform_class = Magnify1DTransform
+    __doc__ = MagnifyCamera.__doc__
diff --git a/vispy/scene/cameras/tests/test_perspective.py b/vispy/scene/cameras/tests/test_perspective.py
new file mode 100644
index 0000000..6fb4798
--- /dev/null
+++ b/vispy/scene/cameras/tests/test_perspective.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+from vispy import scene, io
+from vispy.testing import (requires_application, TestingCanvas,
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
+
+
+ at requires_application()
+def test_perspective_render():
+    with TestingCanvas(size=(300, 200)) as canvas:
+
+        grid = canvas.central_widget.add_grid()
+        grid.padding = 20
+
+        imdata = io.load_crate().astype('float32') / 255
+
+        views = []
+        images = []
+        for i, imethod in enumerate(['impostor', 'subdivide']):
+            for j, vmethod in enumerate(['fragment', 'viewport', 'fbo']):
+                v = grid.add_view(row=i, col=j, border_color='white')
+                v.camera = 'turntable'
+                v.camera.fov = 50
+                v.camera.distance = 30
+                v.clip_method = vmethod
+                
+                views.append(v)
+                image = scene.visuals.Image(imdata, method=imethod, 
+                                            grid=(4, 4))
+                image.transform = scene.STTransform(translate=(-12.8, -12.8),
+                                                    scale=(0.1, 0.1))
+                v.add(image)
+                images.append(image)
+        
+        image = canvas.render()
+        print("ViewBox shapes")
+        for v in views:
+            print(v.node_transform(canvas.canvas_cs).map(v.rect))
+        canvas.close()
+        
+        # Allow many pixels to differ by a small amount--texture sampling and
+        # exact triangle position will differ across platforms. However a 
+        # change in perspective or in the widget borders should trigger a 
+        # failure.
+        assert_image_approved(image, 'scene/cameras/perspective_test.png',
+                              'perspective test 1: 6 identical views with '
+                              'correct perspective',
+                              px_threshold=20,
+                              px_count=60,
+                              max_px_diff=200)
+
+
+run_tests_if_main()
diff --git a/vispy/scene/canvas.py b/vispy/scene/canvas.py
index 1f0af0f..f411945 100644
--- a/vispy/scene/canvas.py
+++ b/vispy/scene/canvas.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
@@ -8,22 +8,18 @@ import weakref
 
 from .. import gloo
 from .. import app
-from .subscene import SubScene
-from .entity import Entity
-from .transforms import STTransform, TransformCache
-from .events import SceneDrawEvent, SceneMouseEvent
+from .node import Node
+from ..visuals.transforms import STTransform, TransformCache
 from ..color import Color
 from ..util import logger
+from ..util.profiler import Profiler
+from .subscene import SubScene
+from .events import SceneDrawEvent, SceneMouseEvent
 from .widgets import Widget
 
 
 class SceneCanvas(app.Canvas):
-    """ SceneCanvas provides a Canvas that automatically draws the contents
-    of a scene.
-
-    Receives the following events:
-    initialize, resize, draw, mouse_press, mouse_release, mouse_move,
-    mouse_wheel, key_press, key_release, stylus, touch, close
+    """A Canvas that automatically draws the contents of a scene
 
     Parameters
     ----------
@@ -46,24 +42,22 @@ class SceneCanvas(app.Canvas):
         Note the canvas application can be accessed at ``canvas.app``.
     create_native : bool
         Whether to create the widget immediately. Default True.
-    init_gloo : bool
-        Initialize standard values in gloo (e.g., ``GL_POINT_SPRITE``).
     vsync : bool
         Enable vertical synchronization.
     resizable : bool
         Allow the window to be resized.
     decorate : bool
-        Decorate the window.
+        Decorate the window. Default True.
     fullscreen : bool | int
         If False, windowed mode is used (default). If True, the default
         monitor is used. If int, the given monitor number is used.
-    context : dict | instance SharedContext | None
-        OpenGL configuration to use when creating the context for the canvas,
-        or a context to share. If None, ``vispy.app.get_default_config`` will
-        be used to set the OpenGL context parameters. Alternatively, the
-        ``canvas.context`` property from an existing canvas (using the
-        same backend) will return a ``SharedContext`` that can be used,
-        thereby sharing the existing context.
+    config : dict
+        A dict with OpenGL configuration options, which is combined
+        with the default configuration options and used to initialize
+        the context. See ``canvas.context.config`` for possible
+        options.
+    shared : Canvas | GLContext | None
+        An existing canvas or context to share OpenGL objects with.
     keys : str | dict | None
         Default key mapping to use. If 'interactive', escape and F11 will
         close the canvas and toggle full-screen mode, respectively.
@@ -72,23 +66,66 @@ class SceneCanvas(app.Canvas):
         be callable.
     parent : widget-object
         The parent widget if this makes sense for the used backend.
+    dpi : float | None
+        Resolution in dots-per-inch to use for the canvas. If dpi is None,
+        then the value will be determined by querying the global config first,
+        and then the operating system.
+    always_on_top : bool
+        If True, try to create the window in always-on-top mode.
+    px_scale : int > 0
+        A scale factor to apply between logical and physical pixels in addition
+        to the actual scale factor determined by the backend. This option
+        allows the scale factor to be adjusted for testing.
     bgcolor : Color
         The background color to use.
 
     See also
     --------
     vispy.app.Canvas
+
+    Notes
+    -----
+    Receives the following events:
+
+        * initialize
+        * resize
+        * draw
+        * mouse_press
+        * mouse_release
+        * mouse_double_click
+        * mouse_move
+        * mouse_wheel
+        * key_press
+        * key_release
+        * stylus
+        * touch
+        * close
+
+    The ordering of the mouse_double_click, mouse_press, and mouse_release
+    events are not guaranteed to be consistent between backends. Only certain
+    backends natively support double-clicking (currently Qt and WX); on other
+    backends, they are detected manually with a fixed time delay.
+    This can cause problems with accessibility, as increasing the OS detection
+    time or using a dedicated double-click button will not be respected.
     """
-    def __init__(self, *args, **kwargs):
+    def __init__(self, title='Vispy canvas', size=(800, 600), position=None,
+                 show=False, autoswap=True, app=None, create_native=True,
+                 vsync=False, resizable=True, decorate=True, fullscreen=False,
+                 config=None, shared=None, keys=None, parent=None, dpi=None,
+                 always_on_top=False, px_scale=1, bgcolor='black'):
         self._fb_stack = []  # for storing information about framebuffers used
         self._vp_stack = []  # for storing information about viewports used
         self._scene = None
-        self._bgcolor = Color(kwargs.pop('bgcolor', 'black')).rgba
         
         # A default widget that follows the shape of the canvas
         self._central_widget = None
 
-        app.Canvas.__init__(self, *args, **kwargs)
+        self._bgcolor = Color(bgcolor).rgba
+
+        super(SceneCanvas, self).__init__(
+            title, size, position, show, autoswap, app, create_native, vsync,
+            resizable, decorate, fullscreen, config, shared, keys, parent, dpi,
+            always_on_top, px_scale)
         self.events.mouse_press.connect(self._process_mouse_event)
         self.events.mouse_move.connect(self._process_mouse_event)
         self.events.mouse_release.connect(self._process_mouse_event)
@@ -98,11 +135,13 @@ class SceneCanvas(app.Canvas):
         # self.draw_visual(...)
         self._transform_caches = weakref.WeakKeyDictionary()
 
-        # Set up default entity stack: ndc -> fb -> canvas -> scene
-        self.render_cs = Entity()
-        self.framebuffer_cs = Entity(parent=self.render_cs)
+        # Set up default node stack: ndc -> fb -> canvas -> scene
+        self.render_cs = Node(name="render_cs")
+        self.framebuffer_cs = Node(parent=self.render_cs, 
+                                   name="framebuffer_cs")
         self.framebuffer_cs.transform = STTransform()
-        self.canvas_cs = Entity(parent=self.framebuffer_cs)
+        self.canvas_cs = Node(parent=self.framebuffer_cs,
+                              name="canvas_cs")
         self.canvas_cs.transform = STTransform()
         # By default, the document coordinate system is the canvas.
         self.canvas_cs.document = self.canvas_cs
@@ -111,7 +150,7 @@ class SceneCanvas(app.Canvas):
         
     @property
     def scene(self):
-        """ The SubScene object that represents the root entity of the
+        """ The SubScene object that represents the root node of the
         scene graph to be displayed.
         """
         return self._scene
@@ -136,36 +175,97 @@ class SceneCanvas(app.Canvas):
         self.update()
 
     def on_draw(self, event):
-        gloo.clear(color=self._bgcolor, depth=True)
+        """Draw handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The draw event.
+        """
         if self._scene is None:
             return  # Can happen on initialization
         logger.debug('Canvas draw')
+
+        self._draw_scene()
+
+    def render(self, region=None, size=None):
+        """ Render the scene to an offscreen buffer and return the image array.
         
+        Parameters
+        ----------
+        region : tuple | None
+            Specifies the region of the canvas to render. Format is 
+            (x, y, w, h). By default, the entire canvas is rendered.
+        size : tuple | None
+            Specifies the size of the image array to return. If no size is 
+            given, then the size of the *region* is used. This argument allows
+            the scene to be rendered at resolutions different from the native
+            canvas resolution.
+
+        Returns
+        -------
+        image : array
+            Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the 
+            upper-left corner of the rendered region.
+        
+        """
+        # Set up a framebuffer to render to
+        offset = (0, 0) if region is None else region[:2]
+        csize = self.size if region is None else region[2:]
+        size = csize if size is None else size
+        fbo = gloo.FrameBuffer(color=gloo.RenderBuffer(size[::-1]),
+                               depth=gloo.RenderBuffer(size[::-1]))
+
+        self.push_fbo(fbo, offset, csize)
+        try:
+            self._draw_scene(viewport=(0, 0) + size)
+            return fbo.read()
+        finally:
+            self.pop_fbo()
+
+    def _draw_scene(self, viewport=None):
+        self.context.clear(color=self._bgcolor, depth=True)
         # Draw the scene, but first disconnect its change signal--
         # any changes that take place during the paint should not trigger
         # a subsequent repaint.
         with self.scene.events.update.blocker(self._scene_update):
-            self.draw_visual(self.scene)
-        
-        if len(self._vp_stack) > 0:
-            logger.warning("Viewport stack not fully cleared after draw.")
-        if len(self._fb_stack) > 0:
-            logger.warning("Framebuffer stack not fully cleared after draw.")
+            self.draw_visual(self.scene, viewport=viewport)
 
-    def draw_visual(self, visual, event=None):
-        """ Draw a *visual* and its children on the canvas.
+    def draw_visual(self, visual, event=None, viewport=None):
+        """ Draw a visual to the canvas or currently active framebuffer.
+        
+        Parameters
+        ----------
+        visual : Visual
+            The visual to draw
+        event : None or DrawEvent
+            Optionally specifies the original canvas draw event that initiated
+            this draw.
+        viewport : tuple | None
+            Optionally specifies the viewport to use. If None, the entire
+            physical size is used.
         """
+        self.set_current()
+        prof = Profiler()
+        nfb = len(self._fb_stack)
+        nvp = len(self._vp_stack)
+        
         # Create draw event, which keeps track of the path of transforms
-        self._process_entity_count = 0  # for debugging
+        self._process_node_count = 0  # for debugging
         
         # Get the cache of transforms used for this visual
         tr_cache = self._transform_caches.setdefault(visual, TransformCache())
         # and mark the entire cache as aged
         tr_cache.roll()
+        prof('roll transform cache')
         
         scene_event = SceneDrawEvent(canvas=self, event=event, 
                                      transform_cache=tr_cache)
-        scene_event.push_viewport((0, 0) + self.size)
+        prof('create SceneDrawEvent')
+        
+        vp = (0, 0) + self.physical_size if viewport is None else viewport
+        scene_event.push_viewport(vp)
+        prof('push_viewport')
         try:
             # Force update of transforms on base entities
             # TODO: this should happen as a reaction to resize, push_viewport,
@@ -174,39 +274,63 @@ class SceneCanvas(app.Canvas):
             self.fb_ndc_transform
             self.canvas_fb_transform
             
-            scene_event.push_entity(self.render_cs)
-            scene_event.push_entity(self.framebuffer_cs)
-            scene_event.push_entity(self.canvas_cs)
-            scene_event.push_entity(visual)
+            scene_event.push_node(self.render_cs)
+            scene_event.push_node(self.framebuffer_cs)
+            scene_event.push_node(self.canvas_cs)
+            scene_event.push_node(visual)
+            prof('initialize event scenegraph')
             visual.draw(scene_event)
+            prof('draw scene')
         finally:
             scene_event.pop_viewport()
 
+        if len(self._vp_stack) > nvp:
+            logger.warning("Viewport stack not fully cleared after draw.")
+        if len(self._fb_stack) > nfb:
+            logger.warning("Framebuffer stack not fully cleared after draw.")
+
     def _process_mouse_event(self, event):
+        prof = Profiler()
         tr_cache = self._transform_caches.setdefault(self.scene, 
                                                      TransformCache())
         scene_event = SceneMouseEvent(canvas=self, event=event,
                                       transform_cache=tr_cache)
-        scene_event.push_entity(self.render_cs)
-        scene_event.push_entity(self.framebuffer_cs)
-        scene_event.push_entity(self.canvas_cs)
-        scene_event.push_entity(self._scene)
+        scene_event.push_node(self.render_cs)
+        scene_event.push_node(self.framebuffer_cs)
+        scene_event.push_node(self.canvas_cs)
+        scene_event.push_node(self._scene)
+        prof('prepare mouse event')
+        
         self._scene._process_mouse_event(scene_event)
+        prof('process')
         
         # If something in the scene handled the scene_event, then we mark
         # the original event accordingly.
         event.handled = scene_event.handled
 
     def on_resize(self, event):
+        """Resize handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The resize event.
+        """
         if self._central_widget is not None:
             self._central_widget.size = self.size
 
     # -------------------------------------------------- transform handling ---
     def push_viewport(self, viewport):
-        """ Push a viewport (x, y, w, h) on the stack. It is the
-        responsibility of the caller to ensure the given values are
+        """ Push a viewport on the stack
+
+        It is the responsibility of the caller to ensure the given values are
         int. The viewport's origin is defined relative to the current
         viewport.
+
+        Parameters
+        ----------
+        viewport : tuple
+            The viewport as (x, y, w, h).
         """
         vp = list(viewport)
         # Normalize viewport before setting;
@@ -216,7 +340,7 @@ class SceneCanvas(app.Canvas):
         if vp[3] < 0:
             vp[1] += vp[3]
             vp[3] *= -1
-            
+
         self._vp_stack.append(vp)
         self.fb_ndc_transform  # update!
         # Apply
@@ -236,18 +360,26 @@ class SceneCanvas(app.Canvas):
             self._set_viewport(self._vp_stack[-1])
             self.fb_ndc_transform  # update!
         return vp
-    
+
     def _set_viewport(self, vp):
-        from .. import gloo
-        gloo.set_viewport(*vp)
+        self.context.set_viewport(*vp)
 
     def push_fbo(self, fbo, offset, csize):
         """ Push an FBO on the stack, together with the new viewport.
         and the transform to the FBO.
+
+        Parameters
+        ----------
+        fbo : instance of FrameBuffer
+            The framebuffer.
+        offset : tuple
+            The offset.
+        csize : tuple
+            The size to use.
         """
         self._fb_stack.append((fbo, offset, csize))
         self.canvas_fb_transform  # update!
-        
+
         # Apply
         try:
             fbo.activate()
@@ -298,8 +430,7 @@ class SceneCanvas(app.Canvas):
         """
         fbo, offset, csize = self._current_framebuffer()
         if fbo is None:
-            # todo: account for high-res displays here.
-            fbsize = csize
+            fbsize = self.physical_size
         else:
             fbsize = fbo.color_buffer.shape
             # image shape is (rows, cols), unlike canvas shape.
@@ -336,3 +467,13 @@ class SceneCanvas(app.Canvas):
         Most visuals should use this transform when drawing.
         """
         return self.fb_ndc_transform * self.canvas_fb_transform
+
+    @property
+    def bgcolor(self):
+        return Color(self._bgcolor)
+
+    @bgcolor.setter
+    def bgcolor(self, color):
+        self._bgcolor = Color(color).rgba
+        if hasattr(self, '_backend'):
+            self.update()
diff --git a/vispy/scene/entity.py b/vispy/scene/entity.py
deleted file mode 100644
index 3f6bb41..0000000
--- a/vispy/scene/entity.py
+++ /dev/null
@@ -1,331 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from __future__ import division
-
-from . import transforms
-from ..util.event import EmitterGroup, Event
-from .events import SceneDrawEvent, SceneMouseEvent
-from .transforms import NullTransform, create_transform
-
-
-class Entity(object):
-    """ Base class to represent a citizen of a scene.
-
-    Typically an Entity is used to visualize something, although this is not
-    strictly necessary. It may for instance also be used as a container to
-    apply a certain transformation to a group of objects, or an object that
-    performs a specific task without being visible.
-
-    Each entity can have zero or more children. Each entity will
-    typically have one parent, although multiple parents are allowed.
-    It is recommended to use multi-parenting with care.
-
-    Parameters
-    ----------
-    parent : Entity
-        The parent of the Entity.
-    name : str
-        The name used to identify the entity.
-    """
-
-    def __init__(self, parent=None, name=None):
-        self.events = EmitterGroup(source=self,
-                                   auto_connect=True,
-                                   parents_change=Event,
-                                   active_parent_change=Event,
-                                   children_change=Event,
-                                   mouse_press=SceneMouseEvent,
-                                   mouse_move=SceneMouseEvent,
-                                   mouse_release=SceneMouseEvent,
-                                   mouse_wheel=SceneMouseEvent,
-                                   draw=SceneDrawEvent,
-                                   children_drawn=SceneDrawEvent,
-                                   update=Event,
-                                   transform_change=Event,
-                                   )
-        self.name = name
-
-        # Entities are organized in a parent-children hierarchy
-        # todo: I think we want this to be a list. The order *may* be important
-        # for some drawing systems. Using a set may lead to inconsistency
-        self._children = set()
-        # TODO: use weakrefs for parents.
-        self._parents = set()
-        if parent is not None:
-            self.parents = parent
-            
-        self._document = None
-
-        # Components that all entities in vispy have
-        # todo: default transform should be trans-scale-rot transform
-        self._transform = transforms.NullTransform()
-    
-    @property
-    def name(self):
-        return self._name
-
-    @name.setter
-    def name(self, n):
-        self._name = n
-
-    @property
-    def children(self):
-        """ The list of children of this entity. The children are in
-        arbitrary order.
-        """
-        return list(self._children)
-
-    @property
-    def parent(self):
-        """ Get/set the parent. If the entity has multiple parents while
-        using this property as a getter, an error is raised.
-        """
-        if not self._parents:
-            return None
-        elif len(self._parents) == 1:
-            return tuple(self._parents)[0]
-        else:
-            raise RuntimeError('Ambiguous parent: there are multiple parents.')
-
-    @parent.setter
-    def parent(self, parent):
-        # This is basically an alias
-        self.parents = parent
-
-    @property
-    def parents(self):
-        """ Get/set a tuple of parents.
-        """
-        return tuple(self._parents)
-
-    @parents.setter
-    def parents(self, parents):
-        # Test input
-        if isinstance(parents, Entity):
-            parents = (parents,)
-        if not hasattr(parents, '__iter__'):
-            raise ValueError("Entity.parents must be iterable (got %s)"
-                             % type(parents))
-
-        # Test that all parents are entities
-        for p in parents:
-            if not isinstance(p, Entity):
-                raise ValueError('A parent of an entity must be an entity too,'
-                                 ' not %s.' % p.__class__.__name__)
-
-        # convert to set
-        prev = self._parents.copy()
-        parents = set(parents)
-
-        with self.events.parents_change.blocker():
-            # Remove from parents
-            for parent in prev - parents:
-                self.remove_parent(parent)
-            # Add new
-            for parent in parents - prev:
-                self.add_parent(parent)
-
-        self.events.parents_change(new=parents, old=prev)
-
-    def add_parent(self, parent):
-        if parent in self._parents:
-            return
-        self._parents.add(parent)
-        parent._add_child(self)
-        self.events.parents_change(added=parent)
-        self.update()
-
-    def remove_parent(self, parent):
-        if parent not in self._parents:
-            raise ValueError("Parent not in set of parents for this entity.")
-        self._parents.remove(parent)
-        parent._remove_child(self)
-        self.events.parents_change(removed=parent)
-
-    def _add_child(self, ent):
-        self._children.add(ent)
-        self.events.children_change(added=ent)
-        ent.events.update.connect(self.events.update)
-
-    def _remove_child(self, ent):
-        self._children.remove(ent)
-        self.events.children_change(removed=ent)
-        ent.events.update.disconnect(self.events.update)
-
-    @property
-    def document(self):
-        """ The document is an optional property that is an entity representing
-        the coordinate system from which this entity should make physical 
-        measurements such as px, mm, pt, in, etc. This coordinate system 
-        should be used when determining line widths, font sizes, and any
-        other lengths specified in physical units.
-        
-        The default is None; in this case, a default document is used during
-        drawing (usually this is supplied by the SceneCanvas).
-        """
-        return self._document
-    
-    @document.setter
-    def document(self, doc):
-        if doc is not None and not isinstance(doc, Entity):
-            raise TypeError("Document property must be Entity or None.")
-        self._document = doc
-        self.update()
-
-    @property
-    def transform(self):
-        """ The transform that maps the local coordinate frame to the
-        coordinate frame of the parent.
-        """
-        return self._transform
-
-    @transform.setter
-    def transform(self, tr):
-        if self._transform is not None:
-            self._transform.changed.disconnect(self._transform_changed)
-        assert isinstance(tr, transforms.BaseTransform)
-        self._transform = tr
-        self._transform.changed.connect(self._transform_changed)
-        self._transform_changed(None)
-
-    def set_transform(self, type, *args, **kwds):
-        """ Create a new transform of *type* and assign it to this entity.
-        All extra arguments are used in the construction of the transform.
-        """
-        self.transform = create_transform(type, *args, **kwds)
-
-    def _transform_changed(self, event):
-        self.events.transform_change()
-        self.update()
-
-    def _parent_chain(self):
-        """
-        Return the chain of parents starting from this entity. The chain ends
-        at the first entity with either no parents or multiple parents.
-        """
-        chain = [self]
-        while True:
-            try:
-                parent = chain[-1].parent
-            except Exception:
-                break
-            if parent is None:
-                break
-            chain.append(parent)
-        return chain
-
-    def describe_tree(self, with_transform=False):
-        """Create tree diagram of children
-
-        Parameters
-        ----------
-        with_transform : bool
-            If true, add information about entity transform types.
-
-        Returns
-        ----------
-        tree : str
-            The tree diagram.
-        """
-        # inspired by https://github.com/mbr/asciitree/blob/master/asciitree.py
-        return self._describe_tree('', with_transform)
-
-    def _describe_tree(self, prefix, with_transform):
-        """Helper function to actuall construct the tree"""
-        extra = ': "%s"' % self.name if self.name is not None else ''
-        if with_transform:
-            extra += (' [%s]' % self.transform.__class__.__name__)
-        output = ''
-        if len(prefix) > 0:
-            output += prefix[:-3]
-            output += '  +--'
-        output += '%s%s\n' % (self.__class__.__name__, extra)
-
-        n_children = len(self.children)
-        for ii, child in enumerate(self.children):
-            sub_prefix = prefix + ('   ' if ii+1 == n_children else '  |')
-            output += child._describe_tree(sub_prefix, with_transform)
-        return output
-
-    def common_parent(self, entity):
-        """
-        Return the common parent of two entities. If the entities have no
-        common parent, return None. Does not search past multi-parent branches.
-        """
-        p1 = self._parent_chain()
-        p2 = entity._parent_chain()
-        for p in p1:
-            if p in p2:
-                return p
-        return None
-        
-    def entity_transform(self, entity):
-        """
-        Return the transform that maps from the coordinate system of
-        *entity* to the local coordinate system of *self*.
-        
-        Note that there must be a _single_ path in the scenegraph that connects
-        the two entities; otherwise an exception will be raised.        
-        """
-        cp = self.common_parent(entity)
-        # First map from entity to common parent
-        tr = NullTransform()
-        
-        while entity is not cp:
-            if entity.transform is not None:
-                tr = entity.transform * tr
-            
-            entity = entity.parent
-        
-        if entity is self:
-            return tr
-        
-        # Now map from common parent to self
-        tr2 = cp.entity_transform(self)
-        return tr2.inverse * tr
-        
-    def _process_mouse_event(self, event):
-        """
-        Propagate a mouse event through the scene tree starting at this Entity.
-        """
-        # 1. find all entities whose mouse-area includes the click point.
-        # 2. send the event to each entity one at a time
-        #    (we should use a specialized emitter for this, rather than
-        #     rebuild the emitter machinery!)
-
-        # TODO: for now we send the event to all entities; need to use
-        # picking to decide which entities should receive the event.
-        for enter, path in self.walk():
-            event._set_path(path)
-            entity = path[-1]
-            getattr(entity.events, event.type)(event)
-
-    def bounds(self, mode, axis):
-        """ Return the (min, max) bounding values describing the location of
-        this entity in its local coordinate system.
-        
-        Parameters
-        ----------
-        mode : str
-            Describes the type of boundary requested. Can be "visual", "data",
-            or "mouse".
-        axis : 0, 1, 2
-            The axis along which to measure the bounding values.
-        
-        Returns
-        -------
-        None or (min, max) tuple. 
-        """
-        return None
-
-    def update(self):
-        """
-        Emit an event to inform Canvases that this Entity needs to be redrawn.
-        """
-        self.events.update()
-
-    def __repr__(self):
-        name = "" if self.name is None else " name="+self.name
-        return "<%s%s at 0x%x>" % (self.__class__.__name__, name, id(self))
diff --git a/vispy/scene/events.py b/vispy/scene/events.py
index 0522bd6..3be444d 100644
--- a/vispy/scene/events.py
+++ b/vispy/scene/events.py
@@ -1,14 +1,14 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
 
 from ..util.event import Event
-from .transforms import TransformCache
+from ..visuals.transforms import TransformCache, TransformSystem
 
 
-class SceneEvent(Event):
+class SceneEvent(Event, TransformSystem):
     """
     SceneEvent is an Event that tracks its path through a scenegraph,
     beginning at a Canvas. It exposes information useful during drawing
@@ -16,19 +16,36 @@ class SceneEvent(Event):
     """
 
     def __init__(self, type, canvas, transform_cache=None):
-        super(SceneEvent, self).__init__(type=type)
+        Event.__init__(self, type=type)
+
+        # Note that we are completely replacing the TransformSystem.__init__
+        # implementation.
         self._canvas = canvas
+        self._dpi = canvas.dpi
 
         # Init stacks
         self._stack = []  # list of entities
         self._stack_ids = set()
         self._viewbox_stack = []
         self._doc_stack = []
+        self._handled_children = []
+
         if transform_cache is None:
             transform_cache = TransformCache()
         self._transform_cache = transform_cache
 
     @property
+    def handled_children(self):
+        """ List of children of the current node that have already been
+        handled.
+
+        Nodes that manually process their children (as opposed to allowing
+        drawing / mouse systems to handle them automatically) may append nodes
+        to this list to prevent systems from handling them.
+        """
+        return self._handled_children
+
+    @property
     def canvas(self):
         """ The Canvas that originated this SceneEvent
         """
@@ -50,20 +67,22 @@ class SceneEvent(Event):
         """
         return self._stack
 
-    def push_entity(self, entity):
-        """ Push an entity on the stack. """
-        self._stack.append(entity)
-        if id(entity) in self._stack_ids:
-            raise RuntimeError("Scenegraph cycle detected; cannot push %r" % 
-                               entity)
-        self._stack_ids.add(id(entity))
-        doc = entity.document
+    def push_node(self, node):
+        """ Push an node on the stack. """
+        self._stack.append(node)
+        self._handled_children.append([])
+        if id(node) in self._stack_ids:
+            raise RuntimeError("Scenegraph cycle detected; cannot push %r" %
+                               node)
+        self._stack_ids.add(id(node))
+        doc = node.document
         if doc is not None:
             self.push_document(doc)
 
-    def pop_entity(self):
-        """ Pop an entity from the stack. """
+    def pop_node(self):
+        """ Pop an node from the stack. """
         ent = self._stack.pop(-1)
+        self._handled_children.pop(-1)
         self._stack_ids.remove(id(ent))
         if ent.document is not None:
             assert ent.document == self.pop_document()
@@ -111,17 +130,20 @@ class SceneEvent(Event):
 
     @property
     def document_cs(self):
-        """ The entity for the current document coordinate system. The
-        coordinate system of this Entity is used for making physical
+        """ The node for the current document coordinate system. The
+        coordinate system of this Node is used for making physical
         measurements--px, mm, in, etc.
         """
-        return self._doc_stack[-1]
+        if len(self._doc_stack) > 0:
+            return self._doc_stack[-1]
+        else:
+            return self.canvas_cs
 
     @property
     def canvas_cs(self):
-        """ The entity for the current canvas coordinate system. This cs 
-        represents the logical pixels of the canvas being drawn, with the 
-        origin in upper-left, and the canvas (width, height) in the bottom 
+        """ The node for the current canvas coordinate system. This cs
+        represents the logical pixels of the canvas being drawn, with the
+        origin in upper-left, and the canvas (width, height) in the bottom
         right. This coordinate system is most often used for handling mouse
         input.
         """
@@ -129,7 +151,7 @@ class SceneEvent(Event):
 
     @property
     def framebuffer_cs(self):
-        """ The entity for the current framebuffer coordinate system. This
+        """ The node for the current framebuffer coordinate system. This
         coordinate system corresponds to the physical pixels being rendered
         to, with the origin in lower-right, and the framebufer (width, height)
         in upper-left. It is used mainly for making antialiasing measurements.
@@ -138,82 +160,74 @@ class SceneEvent(Event):
 
     @property
     def render_cs(self):
-        """ Return entity for the normalized device coordinate system. This
-        coordinate system is the obligatory output of GLSL vertex shaders, 
+        """ Return node for the normalized device coordinate system. This
+        coordinate system is the obligatory output of GLSL vertex shaders,
         with (-1, -1) in bottom-left, and (1, 1) in top-right. This coordinate
         system is frequently used for rendering visuals because all vertices
         must ultimately be mapped here.
         """
         return self.canvas.render_cs
 
-    def document_transform(self, entity=None):
-        """ Return the transform that maps from *entity* to the current
-        document coordinate system.
-
-        If *entity* is not specified, then the top entity on the stack is used.
+    @property
+    def node_cs(self):
+        """ The node at the top of the node stack.
         """
-        return self.entity_transform(map_to=self.document_cs, map_from=entity)
-
-    def map_entity_to_document(self, entity, obj):
-        return self.document_transform(entity).map(obj)
+        return self._stack[-1]
 
-    def map_document_to_entity(self, entity, obj):
-        return self.document_transform(entity).imap(obj)
-
-    def map_to_document(self, obj):
-        return self.document_transform().map(obj)
-
-    def map_from_document(self, obj):
-        return self.document_transform().imap(obj)
-
-    def canvas_transform(self, entity=None):
-        """ Return the transform that maps from *entity* to the current
-        logical-pixel coordinate system defined by the Canvas.
-
-        Canvas_transform is used mainly for mouse interaction.
-        For measuring distance in physical units, the use of document_transform
-        is preferred.
-
-        If *entity* is not specified, then the top entity on the stack is used.
+    @property
+    def visual_to_canvas(self):
+        """ Transform mapping from visual local coordinate frame to canvas
+        coordinate frame.
         """
-        return self.entity_transform(map_to=self.canvas_cs, map_from=entity)
-
-    def map_entity_to_canvas(self, entity, obj):
-        return self.canvas_transform(entity).map(obj)
-
-    def map_canvas_to_entity(self, entity, obj):
-        return self.canvas_transform(entity).imap(obj)
-
-    def map_to_canvas(self, obj):
-        return self.canvas_transform().map(obj)
+        return self.node_transform(map_to=self.canvas_cs,
+                                   map_from=self._stack[-1])
 
-    def map_from_canvas(self, obj):
-        return self.canvas_transform().imap(obj)
+    @property
+    def visual_to_document(self):
+        """ Transform mapping from visual local coordinate frame to document
+        coordinate frame.
+        """
+        return self.node_transform(map_to=self.document_cs,
+                                   map_from=self._stack[-1])
 
-    def framebuffer_transform(self, entity=None):
-        """ Return the transform that maps from *entity* to the current
-        framebuffer coordinate system.
+    @visual_to_document.setter
+    def visual_to_document(self, tr):
+        raise RuntimeError("Cannot set transforms on SceneEvent.")
 
-        If *entity* is not specified, then the top entity on the stack is used.
+    @property
+    def document_to_framebuffer(self):
+        """ Transform mapping from document coordinate frame to the framebuffer
+        (physical pixel) coordinate frame.
         """
-        return self.entity_transform(map_to=self.framebuffer_cs, 
-                                     map_from=entity)
+        return self.node_transform(map_to=self.framebuffer_cs,
+                                   map_from=self.document_cs)
 
-    def map_entity_to_framebuffer(self, entity, obj):
-        return self.framebuffer_transform(entity).map(obj)
+    @document_to_framebuffer.setter
+    def document_to_framebuffer(self, tr):
+        raise RuntimeError("Cannot set transforms on SceneEvent.")
 
-    def map_framebuffer_to_entity(self, entity, obj):
-        return self.framebuffer_transform(entity).imap(obj)
-
-    def map_to_framebuffer(self, obj):
-        return self.framebuffer_transform().map(obj)
+    @property
+    def framebuffer_to_render(self):
+        """ Transform mapping from pixel coordinate frame to rendering
+        coordinate frame.
+        """
+        return self.node_transform(map_to=self.render_cs,
+                                   map_from=self.framebuffer_cs)
 
-    def map_from_framebuffer(self, obj):
-        return self.framebuffer_transform().imap(obj)
+    @framebuffer_to_render.setter
+    def framebuffer_to_render(self, tr):
+        raise RuntimeError("Cannot set transforms on SceneEvent.")
 
     @property
-    def render_transform(self):
-        """ The transform that maps from the current entity to
+    def visual_to_framebuffer(self):
+        """ Transform mapping from visual coordinate frame to the framebuffer
+        (physical pixel) coordinate frame.
+        """
+        return self.node_transform(map_to=self.framebuffer_cs, 
+                                   map_from=self._stack[-1])
+        
+    def get_full_transform(self):
+        """ Return the transform that maps from the current node to
         normalized device coordinates within the current glViewport and
         FBO.
 
@@ -226,107 +240,118 @@ class SceneEvent(Event):
 
     @property
     def scene_transform(self):
-        """ The transform that maps from the current entity to the first
+        """ The transform that maps from the current node to the first
         scene in its ancestry.
         """
-        view = self._viewbox_stack[-1]
-        return self.entity_transform(map_to=view.scene)
+        if len(self._viewbox_stack) > 1:
+            view = self._viewbox_stack[-1]
+            return self.node_transform(map_to=view.scene)
+        else:
+            return None
 
     @property
     def view_transform(self):
-        """ The transform that maps from the current entity to the first
+        """ The transform that maps from the current node to the first
         viewbox in its ancestry.
         """
-        view = self._viewbox_stack[-1]
-        return self.entity_transform(map_to=view)
+        if len(self._viewbox_stack) > 1:
+            view = self._viewbox_stack[-1]
+            return self.node_transform(map_to=view)
+        else:
+            return None
 
-    def entity_transform(self, map_to=None, map_from=None):
+    def node_transform(self, map_to=None, map_from=None):
         """ Return the transform from *map_from* to *map_to*, using the
-        current entity stack to resolve parent ambiguities if needed.
+        current node stack to resolve parent ambiguities if needed.
 
         By default, *map_to* is the normalized device coordinate system,
-        and *map_from* is the current top entity on the stack.
+        and *map_from* is the current top node on the stack.
         """
         if map_to is None:
             map_to = self.render_cs
         if map_from is None:
             map_from = self._stack[-1]
+        fwd_path = self._node_path(map_from, map_to)
 
-        fwd_path = self._entity_path(map_from, map_to)
-        fwd_path.reverse()
-
-        if fwd_path[0] is map_to:
+        if fwd_path[-1] is map_to:
+            fwd_path = fwd_path[:-1]
             rev_path = []
-            fwd_path = fwd_path[1:]
         else:
             # If we have still not reached the end, try traversing from the
-            # opposite end and stop when paths intersect
-            rev_path = self._entity_path(map_to, self._stack[0])
-            connected = False
-            for i in range(1, len(rev_path)):
-                if rev_path[i] in fwd_path:
-                    rev_path = rev_path[:i]
+            # opposite end. Note the reversed order of start and end
+            rev_path = self._node_path(start=map_to, end=map_from)
+            if rev_path[-1] is map_from:
+                fwd_path = []
+                rev_path = rev_path[:-1]
+
+            else:
+                # Find earliest intersection of fwd and rev paths
+                connected = False
+                while fwd_path[-1] is rev_path[-1]:
                     connected = True
+                    fwd_path = fwd_path[:-1]
+                    rev_path = rev_path[:-1]
 
-            if not connected:
-                raise RuntimeError("Unable to find unique path from %r to %r" %
-                                   (map_from, map_to))
+                if not connected:
+                    raise RuntimeError("Unable to find unique path from %r to "
+                                       "%r" % (map_from, map_to))
 
-        transforms = ([e.transform for e in fwd_path] +
-                      [e.transform.inverse for e in rev_path])
+        # starting node must come _last_ in the transform chain
+        fwd_path = fwd_path[::-1]
+        transforms = ([e.transform.inverse for e in rev_path] +
+                      [e.transform for e in fwd_path])
         return self._transform_cache.get(transforms)
 
-    def _entity_path(self, start, end):
+    def _node_path(self, start, end):
         """
         Return the path of parents leading from *start* to *end*, using the
-        entity stack to resolve multi-parent branches.
+        node stack to resolve multi-parent branches.
 
         If *end* is never reached, then the path is assembled as far as
         possible and returned.
         """
         path = [start]
 
-        # first, get parents directly from entity
-        entity = start
-        while id(entity) not in self._stack_ids:
-            if entity is end or len(entity.parents) != 1:
+        # first, get parents directly from node
+        node = start
+        while id(node) not in self._stack_ids:
+            if node is end or len(node.parents) != 1:
                 return path
-            entity = entity.parent
-            path.append(entity)
+            node = node.parent
+            path.append(node)
 
         # if we have not reached the end, follow _stack if possible.
         if path[-1] is not end:
             try:
-                ind = self._stack.index(entity)
-                # copy stack onto path one entity at a time
-                while ind > -1 and path[-1] is not end:
+                ind = self._stack.index(node)
+                # copy stack onto path one node at a time
+                while ind > 0 and path[-1] is not end:
                     ind -= 1
                     path.append(self._stack[ind])
             except IndexError:
                 pass
-
         return path
 
 
 class SceneDrawEvent(SceneEvent):
-    def __init__(self, event, canvas, **kwds):
+    def __init__(self, event, canvas, **kwargs):
         self.draw_event = event
         super(SceneDrawEvent, self).__init__(type='draw', canvas=canvas,
-                                             **kwds)
+                                             **kwargs)
 
 
 class SceneMouseEvent(SceneEvent):
     """ Represents a mouse event that occurred on a SceneCanvas. This event is
-    delivered to all entities whose mouse interaction area is under the event. 
+    delivered to all entities whose mouse interaction area is under the event.
     """
-    def __init__(self, event, canvas, **kwds):
+    def __init__(self, event, canvas, **kwargs):
         self.mouse_event = event
         super(SceneMouseEvent, self).__init__(type=event.type, canvas=canvas,
-                                              **kwds)
+                                              **kwargs)
 
     @property
     def pos(self):
-        """ The position of this event in the local coordinate system of the 
+        """ The position of this event in the local coordinate system of the
         visual.
         """
         return self.map_from_canvas(self.mouse_event.pos)
@@ -344,7 +369,7 @@ class SceneMouseEvent(SceneEvent):
 
     @property
     def press_event(self):
-        """ The mouse press event that initiated a mouse drag, if any. 
+        """ The mouse press event that initiated a mouse drag, if any.
         """
         if self.mouse_event.press_event is None:
             return None
@@ -373,6 +398,16 @@ class SceneMouseEvent(SceneEvent):
     def copy(self):
         ev = self.__class__(self.mouse_event, self._canvas)
         ev._stack = self._stack[:]
-        #ev._ra_stack = self._ra_stack[:]
+        # ev._ra_stack = self._ra_stack[:]
         ev._viewbox_stack = self._viewbox_stack[:]
         return ev
+
+    def map_to_canvas(self, obj):
+        tr = self.node_transform(map_from=self.node_cs,
+                                 map_to=self.canvas_cs)
+        return tr.map(obj)
+
+    def map_from_canvas(self, obj):
+        tr = self.node_transform(map_from=self.canvas_cs,
+                                 map_to=self.node_cs)
+        return tr.map(obj)
diff --git a/vispy/scene/node.py b/vispy/scene/node.py
new file mode 100644
index 0000000..159d137
--- /dev/null
+++ b/vispy/scene/node.py
@@ -0,0 +1,466 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division
+
+from ..util.event import Event, EmitterGroup
+from ..visuals.transforms import (NullTransform, BaseTransform, 
+                                  ChainTransform, create_transform)
+
+
+class Node(object):
+    """ Base class representing an object in a scene.
+
+    A group of nodes connected through parent-child relationships define a 
+    scenegraph. Nodes may have any number of children or parents, although 
+    it is uncommon to have more than one parent.
+
+    Each Node defines a ``transform`` property, which describes the position,
+    orientation, scale, etc. of the Node relative to its parent. The Node's
+    children inherit this property, and then further apply their own
+    transformations on top of that. 
+    
+    With the ``transform`` property, each Node implicitly defines a "local" 
+    coordinate system, and the Nodes and edges in the scenegraph can be though
+    of as coordinate systems connected by transformation functions.
+    
+    Parameters
+    ----------
+    parent : Node
+        The parent of the Node.
+    name : str
+        The name used to identify the node.
+    """
+    
+    # Needed to allow subclasses to repr() themselves before Node.__init__()
+    _name = None
+
+    def __init__(self, parent=None, name=None):
+        self.name = name
+        self._visible = True
+
+        # Add some events to the emitter groups:
+        events = ['parents_change', 'children_change', 'transform_change',
+                  'mouse_press', 'mouse_move', 'mouse_release', 'mouse_wheel', 
+                  'key_press', 'key_release']
+        # Create event emitter if needed (in subclasses that inherit from
+        # Visual, we already have an emitter to share)
+        if not hasattr(self, 'events'):
+            self.events = EmitterGroup(source=self, auto_connect=True,
+                                       update=Event)
+        self.events.add(**dict([(ev, Event) for ev in events]))
+        
+        # Entities are organized in a parent-children hierarchy
+        self._children = []
+        # TODO: use weakrefs for parents.
+        self._parents = []
+        if parent is not None:
+            self.parents = parent
+            
+        self._document = None
+
+        # Components that all entities in vispy have
+        # todo: default transform should be trans-scale-rot transform
+        self._transform = NullTransform()
+    
+    # todo: move visible to BaseVisualNode class when we make Node not a Visual
+    @property
+    def visible(self):
+        """ Whether this node should be drawn or not. Only applicable to
+        nodes that can be drawn.
+        """
+        return self._visible
+    
+    @visible.setter
+    def visible(self, val):
+        self._visible = bool(val)
+        self.update()
+    
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, n):
+        self._name = n
+
+    @property
+    def children(self):
+        """ A copy of the list of children of this node. Do not add
+        items to this list, but use ``x.parent = y`` instead.
+        """
+        return list(self._children)
+
+    @property
+    def parent(self):
+        """ Get/set the parent. If the node has multiple parents while
+        using this property as a getter, an error is raised.
+        """
+        if not self._parents:
+            return None
+        elif len(self._parents) == 1:
+            return self._parents[0]
+        else:
+            raise RuntimeError('Ambiguous parent: there are multiple parents.')
+
+    @parent.setter
+    def parent(self, parent):
+        # This is basically an alias
+        self.parents = parent
+
+    @property
+    def parents(self):
+        """ Get/set a tuple of parents.
+        """
+        return tuple(self._parents)
+
+    @parents.setter
+    def parents(self, parents):
+        # Test input
+        if isinstance(parents, Node):
+            parents = (parents,)
+        if not hasattr(parents, '__iter__'):
+            raise ValueError("Node.parents must be iterable (got %s)"
+                             % type(parents))
+
+        # Test that all parents are entities
+        for p in parents:
+            if not isinstance(p, Node):
+                raise ValueError('A parent of an node must be an node too,'
+                                 ' not %s.' % p.__class__.__name__)
+
+        # Apply
+        prev = list(self._parents)  # No list.copy() on Py2.x
+        with self.events.parents_change.blocker():
+            # Remove parents
+            for parent in prev:
+                if parent not in parents:
+                    self.remove_parent(parent)
+            # Add new parents
+            for parent in parents:
+                if parent not in prev:
+                    self.add_parent(parent)
+
+        self.events.parents_change(new=parents, old=prev)
+
+    def add_parent(self, parent):
+        """Add a parent
+
+        Parameters
+        ----------
+        parent : instance of Node
+            The parent.
+        """
+        if parent in self._parents:
+            return
+        self._parents.append(parent)
+        parent._add_child(self)
+        self.events.parents_change(added=parent)
+        self.update()
+
+    def remove_parent(self, parent):
+        """Remove a parent
+
+        Parameters
+        ----------
+        parent : instance of Node
+            The parent.
+        """
+        if parent not in self._parents:
+            raise ValueError("Parent not in set of parents for this node.")
+        self._parents.remove(parent)
+        parent._remove_child(self)
+        self.events.parents_change(removed=parent)
+
+    def _add_child(self, node):
+        self._children.append(node)
+        self.events.children_change(added=node)
+        node.events.update.connect(self.events.update)
+
+    def _remove_child(self, node):
+        self._children.remove(node)
+        self.events.children_change(removed=node)
+        node.events.update.disconnect(self.events.update)
+
+    def update(self):
+        """
+        Emit an event to inform listeners that properties of this Node or its
+        children have changed.
+        """
+        self.events.update()
+
+    @property
+    def document(self):
+        """ The document is an optional property that is an node representing
+        the coordinate system from which this node should make physical 
+        measurements such as px, mm, pt, in, etc. This coordinate system 
+        should be used when determining line widths, font sizes, and any
+        other lengths specified in physical units.
+        
+        The default is None; in this case, a default document is used during
+        drawing (usually this is supplied by the SceneCanvas).
+        """
+        return self._document
+    
+    @document.setter
+    def document(self, doc):
+        if doc is not None and not isinstance(doc, Node):
+            raise TypeError("Document property must be Node or None.")
+        self._document = doc
+        self.update()
+
+    @property
+    def transform(self):
+        """ The transform that maps the local coordinate frame to the
+        coordinate frame of the parent.
+        """
+        return self._transform
+
+    @transform.setter
+    def transform(self, tr):
+        if self._transform is not None:
+            self._transform.changed.disconnect(self._transform_changed)
+        assert isinstance(tr, BaseTransform)
+        self._transform = tr
+        self._transform.changed.connect(self._transform_changed)
+        self._transform_changed(None)
+
+    def set_transform(self, type_, *args, **kwargs):
+        """ Create a new transform of *type* and assign it to this node.
+
+        All extra arguments are used in the construction of the transform.
+
+        Parameters
+        ----------
+        type_ : str
+            The transform type.
+        *args : tuple
+            Arguments.
+        **kwargs : dict
+            Keywoard arguments.
+        """
+        self.transform = create_transform(type_, *args, **kwargs)
+
+    def _transform_changed(self, event):
+        self.events.transform_change()
+        self.update()
+
+    def _parent_chain(self):
+        """
+        Return the chain of parents starting from this node. The chain ends
+        at the first node with either no parents or multiple parents.
+        """
+        chain = [self]
+        while True:
+            try:
+                parent = chain[-1].parent
+            except Exception:
+                break
+            if parent is None:
+                break
+            chain.append(parent)
+        return chain
+
+    def describe_tree(self, with_transform=False):
+        """Create tree diagram of children
+
+        Parameters
+        ----------
+        with_transform : bool
+            If true, add information about node transform types.
+
+        Returns
+        ----------
+        tree : str
+            The tree diagram.
+        """
+        # inspired by https://github.com/mbr/asciitree/blob/master/asciitree.py
+        return self._describe_tree('', with_transform)
+
+    def _describe_tree(self, prefix, with_transform):
+        """Helper function to actuall construct the tree"""
+        extra = ': "%s"' % self.name if self.name is not None else ''
+        if with_transform:
+            extra += (' [%s]' % self.transform.__class__.__name__)
+        output = ''
+        if len(prefix) > 0:
+            output += prefix[:-3]
+            output += '  +--'
+        output += '%s%s\n' % (self.__class__.__name__, extra)
+
+        n_children = len(self.children)
+        for ii, child in enumerate(self.children):
+            sub_prefix = prefix + ('   ' if ii+1 == n_children else '  |')
+            output += child._describe_tree(sub_prefix, with_transform)
+        return output
+
+    def common_parent(self, node):
+        """
+        Return the common parent of two entities
+
+        If the entities have no common parent, return None.
+        Does not search past multi-parent branches.
+
+        Parameters
+        ----------
+        node : instance of Node
+            The other node.
+
+        Returns
+        -------
+        parent : instance of Node | None
+            The parent.
+        """
+        p1 = self._parent_chain()
+        p2 = node._parent_chain()
+        for p in p1:
+            if p in p2:
+                return p
+        return None
+
+    def node_path_to_child(self, node):
+        """Return a list describing the path from this node to a child node
+
+        This method assumes that the given node is a child node. Multiple
+        parenting is allowed.
+
+        Parameters
+        ----------
+        node : instance of Node
+            The child node.
+
+        Returns
+        -------
+        path : list | None
+            The path.
+        """
+        if node is self:
+            return []
+
+        # Go up from the child node as far as we can
+        path1 = [node]
+        child = node
+        while len(child.parents) == 1:
+            child = child.parent
+            path1.append(child)
+            # Early exit
+            if child is self:
+                return list(reversed(path1))
+        
+        # Verify that we're not cut off
+        if len(path1[-1].parents) == 0:
+            raise RuntimeError('%r is not a child of %r' % (node, self))
+        
+        def _is_child(path, parent, child):
+            path.append(parent)
+            if child in parent.children:
+                return path
+            else:
+                for c in parent.children:
+                    possible_path = _is_child(path[:], c, child)
+                    if possible_path:
+                        return possible_path
+            return None
+
+        # Search from the parent towards the child
+        path2 = _is_child([], self, path1[-1])
+        if not path2:
+            raise RuntimeError('%r is not a child of %r' % (node, self))
+
+        # Return
+        return path2 + list(reversed(path1))
+
+    def node_path(self, node):
+        """Return two lists describing the path from this node to another
+
+        Parameters
+        ----------
+        node : instance of Node
+            The other node.
+
+        Returns
+        -------
+        p1 : list
+            First path (see below).
+        p2 : list
+            Second path (see below).
+
+        Notes
+        -----
+        The first list starts with this node and ends with the common parent
+        between the endpoint nodes. The second list contains the remainder of
+        the path from the common parent to the specified ending node.
+        
+        For example, consider the following scenegraph::
+        
+            A --- B --- C --- D
+                   \
+                    --- E --- F
+        
+        Calling `D.node_path(F)` will return::
+        
+            ([D, C, B], [E, F])
+        
+        Note that there must be a _single_ path in the scenegraph that connects
+        the two entities; otherwise an exception will be raised.        
+        """
+        p1 = self._parent_chain()
+        p2 = node._parent_chain()
+        cp = None
+        for p in p1:
+            if p in p2:
+                cp = p
+                break
+        if cp is None:
+            raise RuntimeError("No single-path common parent between nodes %s "
+                               "and %s." % (self, node))
+        
+        p1 = p1[:p1.index(cp)+1]
+        p2 = p2[:p2.index(cp)][::-1]
+        return p1, p2
+        
+    def node_path_transforms(self, node):
+        """Return the list of transforms along the path to another node.
+        
+        The transforms are listed in reverse order, such that the last 
+        transform should be applied first when mapping from this node to 
+        the other.
+
+        Parameters
+        ----------
+        node : instance of Node
+            The other node.
+
+        Returns
+        -------
+        transform : instance of Transform
+            The transform.
+        """
+        a, b = self.node_path(node)
+        return ([n.transform.inverse for n in b] +
+                [n.transform for n in a[:-1]])[::-1]
+
+    def node_transform(self, node):
+        """
+        Return the transform that maps from the coordinate system of
+        *self* to the local coordinate system of *node*.
+
+        Note that there must be a _single_ path in the scenegraph that connects
+        the two entities; otherwise an exception will be raised.
+
+        Parameters
+        ----------
+        node : instance of Node
+            The other node.
+
+        Returns
+        -------
+        transform : instance of ChainTransform
+            The transform.
+        """
+        return ChainTransform(self.node_path_transforms(node))
+
+    def __repr__(self):
+        name = "" if self.name is None else " name="+self.name
+        return "<%s%s at 0x%x>" % (self.__class__.__name__, name, id(self))
diff --git a/vispy/scene/shaders/program.py b/vispy/scene/shaders/program.py
deleted file mode 100644
index f41afb1..0000000
--- a/vispy/scene/shaders/program.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from __future__ import division, print_function
-
-from ...gloo import Program
-from ...util import logger
-from ...util.event import EventEmitter
-from ...ext.six import string_types  # noqa
-from .function import MainFunction, Variable
-from .compiler import Compiler
-
-
-class ModularProgram(Program):
-    """
-    Shader program using Function instances as basis for its shaders.
-    
-    Automatically rebuilds program when functions have changed and uploads 
-    program variables.
-    """
-    def __init__(self, vcode, fcode):
-        Program.__init__(self, '', '')
-        
-        self.changed = EventEmitter(source=self, type='program_change')
-        
-        self.vert = MainFunction(vcode)
-        self.frag = MainFunction(fcode)
-        self.vert.changed.connect(self._source_changed)
-        self.frag.changed.connect(self._source_changed)
-        
-        # Cache state of Variables so we know which ones require update
-        self._variable_state = {}
-        
-        self._need_build = True
-
-    def prepare(self):
-        """ Prepare the Program so we can set attributes and uniforms.
-        """
-        # TEMP function to fix sync issues for now
-        self._create()
-        if self._need_build:
-            self._build()
-            self._need_build = False
-    
-    def _source_changed(self, ev):
-        logger.debug("ModularProgram source changed: %s" % self)
-        if ev.code_changed:
-            self._need_build = True
-        self.changed()
-        
-    def _build(self):
-        logger.debug("Rebuild ModularProgram: %s" % self)
-        self.compiler = Compiler(vert=self.vert, frag=self.frag)
-        code = self.compiler.compile()
-        self.shaders[0].code = code['vert']
-        self.shaders[1].code = code['frag']
-        
-        logger.debug('==== Vertex Shader ====\n\n' + code['vert'] + "\n")
-        logger.debug('==== Fragment shader ====\n\n' + code['frag'] + "\n")
-        
-        self._create_variables()  # force update
-        self._variable_state = {}
-        
-        # and continue.
-        super(ModularProgram, self)._build()
-
-    def _activate_variables(self):
-        # set all variables
-        settable_vars = 'attribute', 'uniform'
-        logger.debug("Apply variables:")
-        deps = self.vert.dependencies() + self.frag.dependencies()
-        for dep in deps:
-            if not isinstance(dep, Variable) or dep.vtype not in settable_vars:
-                continue
-            name = self.compiler[dep]
-            logger.debug("    %s = %s", name, dep.value)
-            state_id = dep.state_id
-            if self._variable_state.get(name, None) != state_id:
-                self[name] = dep.value
-                self._variable_state[name] = state_id
-        
-        super(ModularProgram, self)._activate_variables()        
diff --git a/vispy/scene/subscene.py b/vispy/scene/subscene.py
index 0ca180b..13e6481 100644
--- a/vispy/scene/subscene.py
+++ b/vispy/scene/subscene.py
@@ -1,14 +1,14 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
 
-from .entity import Entity
+from .node import Node
 from .systems import DrawingSystem, MouseInputSystem
 
 
-class SubScene(Entity):
+class SubScene(Node):
     """ A subscene with entities.
 
     A subscene can be a child of a Canvas or a ViewBox. It is a
@@ -29,16 +29,27 @@ class SubScene(Entity):
     """
 
     def __init__(self, **kwargs):
-        Entity.__init__(self, **kwargs)
+        Node.__init__(self, **kwargs)
 
         # Initialize systems
         self._systems = {}
         self._systems['draw'] = DrawingSystem()
         self._systems['mouse'] = MouseInputSystem()
+        self._drawing = False
     
     def draw(self, event):
+        # Temporary workaround to avoid infinite recursion. A better solution
+        # would be for ViewBox and Canvas to handle the systems, rather than
+        # subscene.
+        if self._drawing:
+            return
+        
         # Invoke our drawing system
-        self.process_system(event, 'draw') 
+        try:
+            self._drawing = True
+            self.process_system(event, 'draw')
+        finally:
+            self._drawing = False
     
     def _process_mouse_event(self, event):
         self.process_system(event, 'mouse') 
diff --git a/vispy/scene/systems.py b/vispy/scene/systems.py
index 282be9c..5cb0d66 100644
--- a/vispy/scene/systems.py
+++ b/vispy/scene/systems.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
 
 import sys
 
-from .visuals.visual import Visual
 from ..util.logs import logger, _handle_exception
+from ..util.profiler import Profiler
 
 
 class DrawingSystem(object):
@@ -15,80 +15,61 @@ class DrawingSystem(object):
     per viewbox.
 
     """
-    def process(self, event, subscene):
-        # Iterate over entities
-        #assert isinstance(subscene, SubScene)  # LC: allow any part of the
-                                                #     scene to be drawn
-        self._process_entity(event, subscene, force_recurse=True)
-
-    def _process_entity(self, event, entity, force_recurse=False):
-        event.canvas._process_entity_count += 1
-
-        if isinstance(entity, Visual):
+    def process(self, event, node):
+        prof = Profiler(str(node))
+        # Draw this node if it is a visual
+        if hasattr(node, 'draw') and node.visible:
             try:
-                entity.draw(event)
+                node.draw(event)
+                prof('draw')
             except Exception:
                 # get traceback and store (so we can do postmortem
                 # debugging)
-                _handle_exception(False, 'reminders', self, entity=entity)
+                _handle_exception(False, 'reminders', self, node=node)
 
-        # Processs children; recurse.
-        # Do not go into subscenes (SubScene.draw processes the subscene)
-        
-        # import here to break import cycle.
-        # (LC: we should be able to remove this
-        # check entirely.)
-        from .subscene import SubScene
-        
-        if force_recurse or not isinstance(entity, SubScene):
-            for sub_entity in entity.children:
-                event.push_entity(sub_entity)
-                try:
-                    self._process_entity(event, sub_entity)
-                finally:
-                    event.pop_entity()
+        # Processs children recursively, unless the node has already
+        # handled them.
+        for sub_node in node.children:
+            if sub_node in event.handled_children:
+                continue
+            event.push_node(sub_node)
+            try:
+                self.process(event, sub_node)
+            finally:
+                event.pop_node()
+            prof('process child %s', sub_node)
 
 
 class MouseInputSystem(object):
-    def process(self, event, subscene):
-        # For simplicity, this system delivers the event to each entity
+    def process(self, event, node):
+        # For simplicity, this system delivers the event to each node
         # in the scenegraph, except for widgets that are not under the 
         # press_event. 
-        # TODO: 
-        #  1. This eventually should be replaced with a picking system.
-        #  2. We also need to ensure that if one entity accepts a press 
-        #     event, it will also receive all subsequent mouse events
-        #     until the button is released.
+        # TODO: This eventually should be replaced with a picking system.
         
-        self._process_entity(event, subscene)
-    
-    def _process_entity(self, event, entity):
-        # Push entity and set its total transform
-        #event.push_entity(entity)
-
         from .widgets.widget import Widget
-        if isinstance(entity, Widget):
+        if isinstance(node, Widget):
             # widgets are rectangular; easy to do mouse collision 
             # testing
             if event.press_event is None:
-                deliver = entity.rect.contains(*event.pos[:2])
+                deliver = node.rect.contains(*event.pos[:2])
             else:
-                deliver = entity.rect.contains(*event.press_event.pos[:2])
+                deliver = node.rect.contains(*event.press_event.pos[:2])
         else:
             deliver = True
                 
         if deliver:
-            for sub_entity in entity.children:
-                event.push_entity(sub_entity)
+            for sub_node in node.children:
+                event.push_node(sub_node)
                 try:
-                    self._process_entity(event, sub_entity)
+                    self.process(event, sub_node)
                 finally:
-                    event.pop_entity()
+                    event.pop_node()
                 if event.handled:
                     break
             if not event.handled:
                 try:
-                    getattr(entity.events, event.type)(event)
+                    getattr(node.events, event.type)(event)
                 except Exception:
                     # get traceback and store (so we can do postmortem
                     # debugging)
@@ -100,7 +81,7 @@ class MouseInputSystem(object):
                     del tb  # Get rid of it in this namespace
                     # Handle
                     logger.log_exception()
-                    logger.warning("Error handling mouse event for entity %s" %
-                                   entity)
+                    logger.warning("Error handling mouse event for node %s" %
+                                   node)
                     
-        #event.pop_entity()
+        #event.pop_node()
diff --git a/vispy/scene/tests/test_node.py b/vispy/scene/tests/test_node.py
new file mode 100644
index 0000000..247383b
--- /dev/null
+++ b/vispy/scene/tests/test_node.py
@@ -0,0 +1,35 @@
+from vispy.scene.node import Node
+from vispy.testing import run_tests_if_main
+
+
+def test_graph():
+    # Graph looks like:
+    # 
+    #  a --- b --- c --- d --- g
+    #         \            /
+    #          --- e --- f 
+    #
+    a = Node(name='a')
+    b = Node(name='b', parent=a)
+    c = Node(name='c', parent=b)
+    d = Node(name='d', parent=c)
+    e = Node(name='e', parent=b)
+    f = Node(name='f', parent=e)
+    g = Node(name='g', )
+    g.parents = (f, d)
+    
+    assert a.parent is None
+    assert b.node_path(a) == ([b, a], [])
+    assert a.node_path(b) == ([a], [b])
+    assert c.node_path(a) == ([c, b, a], [])
+    assert a.node_path(c) == ([a], [b, c])
+    assert d.node_path(f) == ([d, c, b], [e, f])
+    assert f.node_path(d) == ([f, e, b], [c, d])
+    try:
+        g.node_path(b)
+        raise Exception("Should have raised RuntimeError")
+    except RuntimeError:
+        pass
+
+
+run_tests_if_main()
diff --git a/vispy/scene/tests/test_visuals.py b/vispy/scene/tests/test_visuals.py
new file mode 100644
index 0000000..e3aacb7
--- /dev/null
+++ b/vispy/scene/tests/test_visuals.py
@@ -0,0 +1,27 @@
+from vispy.scene import visuals, Node
+import vispy.visuals
+
+
+def test_docstrings():
+    # test that docstring insertions worked for all Visual+Node subclasses
+    for name in dir(visuals):
+        obj = getattr(visuals, name)
+        if isinstance(obj, type) and issubclass(obj, Node):
+            if obj is Node:
+                continue
+            assert "This class inherits from visuals." in obj.__doc__
+            assert "parent : Node" in obj.__doc__
+
+
+def test_visual_node_generation():
+    # test that all Visual classes also have Visual+Node classes
+    visuals = []
+    for name in dir(vispy.visuals):
+        obj = getattr(vispy.visuals, name)
+        if isinstance(obj, type) and issubclass(obj, Node):
+            if obj is Node:
+                continue
+            assert name.endswith('Visual')
+            vis_node = getattr(visuals, name[:-6])
+            assert issubclass(vis_node, Node)
+            assert issubclass(vis_node, obj)
diff --git a/vispy/scene/transforms/linear.py b/vispy/scene/transforms/linear.py
deleted file mode 100644
index 92f0623..0000000
--- a/vispy/scene/transforms/linear.py
+++ /dev/null
@@ -1,401 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from __future__ import division
-
-import numpy as np
-
-from ...util import transforms
-from ...geometry import Rect
-from ._util import arg_to_vec4, as_vec4
-from .base_transform import BaseTransform
-
-
-class NullTransform(BaseTransform):
-    """ Transform having no effect on coordinates (identity transform).
-    """
-    glsl_map = "vec4 null_transform_map(vec4 pos) {return pos;}"
-    glsl_imap = "vec4 null_transform_imap(vec4 pos) {return pos;}"
-
-    Linear = True
-    Orthogonal = True
-    NonScaling = True
-    Isometric = True
-
-    def map(self, obj):
-        return obj
-
-    def imap(self, obj):
-        return obj
-
-    def __mul__(self, tr):
-        return tr
-
-    def __rmul__(self, tr):
-        return tr
-
-
-class STTransform(BaseTransform):
-    """ Transform performing only scale and translate, in that order.
-
-    Parameters
-    ----------
-    scale : array-like
-        Scale factors for X, Y, Z axes.
-    translate : array-like
-        Scale factors for X, Y, Z axes.
-    """
-    glsl_map = """
-        vec4 st_transform_map(vec4 pos) {
-            return (pos * $scale) + $translate;
-        }
-    """
-
-    glsl_imap = """
-        vec4 st_transform_imap(vec4 pos) {
-            return (pos - $translate) / $scale;
-        }
-    """
-
-    Linear = True
-    Orthogonal = True
-    NonScaling = False
-    Isometric = False
-
-    def __init__(self, scale=None, translate=None):
-        super(STTransform, self).__init__()
-
-        self._scale = np.ones(4, dtype=np.float32)
-        self._translate = np.zeros(4, dtype=np.float32)
-
-        self.scale = (1.0, 1.0, 1.0) if scale is None else scale
-        self.translate = (0.0, 0.0, 0.0) if translate is None else translate
-
-    @arg_to_vec4
-    def map(self, coords):
-        n = coords.shape[-1]
-        return coords * self.scale[:n] + self.translate[:n]
-
-    @arg_to_vec4
-    def imap(self, coords):
-        n = coords.shape[-1]
-        return (coords - self.translate[:n]) / self.scale[:n]
-
-    def shader_map(self):
-        self._shader_map['scale'] = self.scale
-        self._shader_map['translate'] = self.translate
-        return self._shader_map
-
-    def shader_imap(self):
-        self._shader_imap['scale'] = self.scale
-        self._shader_imap['translate'] = self.translate
-        return self._shader_imap
-
-    @property
-    def scale(self):
-        return self._scale.copy()
-
-    @scale.setter
-    def scale(self, s, update=True):
-        if np.all(s == self._scale[:len(s)]):
-            return
-        self._scale[:len(s)] = s[:4]
-        self._scale[len(s):] = 1.0
-        if update:
-            self.shader_map()  # update shader variables
-            self.shader_imap()
-            self._update()
-
-    @property
-    def translate(self):
-        return self._translate.copy()
-
-    @translate.setter
-    def translate(self, t, update=True):
-        if np.all(t == self._translate[:len(t)]):
-            return
-        self._translate[:len(t)] = t[:4]
-        self._translate[len(t):] = 0.0
-        if update:
-            self.shader_map()  # update shader variables
-            self.shader_imap()
-            self._update()
-
-    def as_affine(self):
-        m = AffineTransform()
-        m.scale(self.scale)
-        m.translate(self.translate)
-        return m
-    
-    def _update(self):
-        # force update of uniforms on shader functions
-        self.shader_map()
-        self.shader_imap()
-        self.update()
-
-    @classmethod
-    def from_mapping(cls, x0, x1):
-        """ Create an STTransform from the given mapping. 
-        See ``set_mapping()`` for details.
-        """
-        t = cls()
-        t.set_mapping(x0, x1)
-        return t
-    
-    def set_mapping(self, x0, x1):
-        """ Configure this transform such that it maps points x0 => x1, 
-        where each argument must be an array of shape (2, 2) or (2, 3).
-        
-        For example, if we wish to map the corners of a rectangle::
-        
-            p1 = [[0, 0], [200, 300]]
-            
-        onto a unit cube::
-        
-            p2 = [[-1, -1], [1, 1]]
-            
-        then we can generate the transform as follows::
-        
-            tr = STTransform()
-            tr.set_mapping(p1, p2)
-            
-            # test:
-            assert tr.map(p1)[:,:2] == p2
-        
-        """
-        # if args are Rect, convert to array first
-        if isinstance(x0, Rect):
-            x0 = x0._transform_in()[:3]
-        if isinstance(x1, Rect):
-            x1 = x1._transform_in()[:3]
-        
-        x0 = np.array(x0)
-        x1 = np.array(x1)
-        denom = (x0[1] - x0[0])
-        mask = denom == 0
-        denom[mask] = 1.0 
-        s = (x1[1] - x1[0]) / denom
-        s[mask] = 1.0
-        s[x0[1] == x0[0]] = 1.0
-        t = x1[0] - s * x0[0]
-        
-        STTransform.scale.fset(self, s, update=False)
-        self.translate = t
-
-    def __mul__(self, tr):
-        if isinstance(tr, STTransform):
-            s = self.scale * tr.scale
-            t = self.translate + (tr.translate * self.scale)
-            return STTransform(scale=s, translate=t)
-        elif isinstance(tr, AffineTransform):
-            return self.as_affine() * tr
-        else:
-            return super(STTransform, self).__mul__(tr)
-
-    def __rmul__(self, tr):
-        if isinstance(tr, AffineTransform):
-            return tr * self.as_affine()
-        return super(STTransform, self).__rmul__(tr)
-
-    def __repr__(self):
-        return ("<STTransform scale=%s translate=%s>"
-                % (self.scale, self.translate))
-
-
-class AffineTransform(BaseTransform):
-    """Affine transformation class
-
-    Parameters
-    ----------
-    matrix : array-like
-        4x4 array to use for the transform.
-    """
-    glsl_map = """
-        vec4 affine_transform_map(vec4 pos) {
-            return $matrix * pos;
-        }
-    """
-
-    glsl_imap = """
-        vec4 affine_transform_imap(vec4 pos) {
-            return $inv_matrix * pos;
-        }
-    """
-
-    Linear = True
-    Orthogonal = False
-    NonScaling = False
-    Isometric = False
-
-    def __init__(self, matrix=None):
-        super(AffineTransform, self).__init__()
-        if matrix is not None:
-            self.matrix = matrix
-        else:
-            self.reset()
-
-    @arg_to_vec4
-    def map(self, coords):
-        # looks backwards, but both matrices are transposed.
-        return np.dot(coords, self.matrix)
-
-    @arg_to_vec4
-    def imap(self, coords):
-        return np.dot(coords, self.inv_matrix)
-
-    def shader_map(self):
-        fn = super(AffineTransform, self).shader_map()
-        fn['matrix'] = self.matrix  # uniform mat4
-        return fn
-
-    def shader_imap(self):
-        fn = super(AffineTransform, self).shader_imap()
-        fn['inv_matrix'] = self.inv_matrix  # uniform mat4
-        return fn
-
-    @property
-    def matrix(self):
-        return self._matrix
-
-    @matrix.setter
-    def matrix(self, m):
-        self._matrix = m
-        self._inv_matrix = None
-        self.shader_map()
-        self.shader_imap()
-        self.update()
-
-    @property
-    def inv_matrix(self):
-        if self._inv_matrix is None:
-            self._inv_matrix = np.linalg.inv(self.matrix)
-        return self._inv_matrix
-
-    @arg_to_vec4
-    def translate(self, pos):
-        """
-        Translate the matrix by *pos*.
-
-        The translation is applied *after* the transformations already present
-        in the matrix.
-        """
-        self.matrix = transforms.translate(self.matrix, *pos[0, :3])
-
-    def scale(self, scale, center=None):
-        """
-        Scale the matrix by *scale* around the origin *center*.
-
-        The scaling is applied *after* the transformations already present
-        in the matrix.
-        """
-        scale = as_vec4(scale, default=(1, 1, 1, 1))
-        if center is not None:
-            center = as_vec4(center)[0, :3]
-            m = transforms.translate(self.matrix, *(-center))
-            m = transforms.scale(m, *scale[0, :3])
-            m = transforms.translate(self.matrix, *center)
-            self.matrix = m
-        else:
-            self.matrix = transforms.scale(self.matrix, *scale[0, :3])
-
-    def rotate(self, angle, axis):
-        #tr = transforms.rotate(np.eye(4), angle, *axis)
-        #self.matrix = np.dot(tr, self.matrix)
-        self.matrix = transforms.rotate(self.matrix, angle, *axis)
-
-    def set_mapping(self, points1, points2):
-        """ Set to a 3D transformation matrix that maps points1 onto points2.
-        
-        Arguments are specified as arrays of four 3D coordinates, shape (4, 3).
-        """
-        # note: need to transpose because util.functions uses opposite
-        # of standard linear algebra order.
-        self.matrix = transforms.affine_map(points1, points2).T
-
-    def set_ortho(self, l, r, b, t, n, f):
-        self.matrix = transforms.ortho(l, r, b, t, n, f)
-
-    def reset(self):
-        self.matrix = np.eye(4)
-
-    def __mul__(self, tr):
-        if (isinstance(tr, AffineTransform) and not 
-                any(tr.matrix[:3, 3] != 0)):   
-            # don't multiply if the perspective column is used
-            return AffineTransform(matrix=np.dot(tr.matrix, self.matrix))
-        else:
-            return tr.__rmul__(self)
-            #return super(AffineTransform, self).__mul__(tr)
-
-    def __repr__(self):
-        s = "%s(matrix=[" % self.__class__.__name__
-        indent = " "*len(s)
-        s += str(list(self.matrix[0])) + ",\n"
-        s += indent + str(list(self.matrix[1])) + ",\n"
-        s += indent + str(list(self.matrix[2])) + ",\n"
-        s += indent + str(list(self.matrix[3])) + "] at 0x%x)" % id(self)
-        return s
-
-
-#class SRTTransform(BaseTransform):
-#    """ Transform performing scale, rotate, and translate, in that order.
-#
-#    This transformation allows objects to be placed arbitrarily in a scene
-#    much the same way AffineTransform does. However, an incorrect order of
-#    operations in AffineTransform may result in shearing the object (if scale
-#    is applied after rotate) or in unpredictable translation (if scale/rotate
-#    is applied after translation). SRTTransform avoids these problems by
-#    enforcing the correct order of operations.
-#    """
-#    # TODO
-
-
-class PerspectiveTransform(AffineTransform):
-    """
-    Matrix transform that also implements perspective division.
-    
-    """
-    # Note: Although OpenGL operates in homogeneouus coordinates, it may be
-    # necessary to manually implement perspective division.. 
-    # Perhaps we can find a way to avoid this.
-    glsl_map = """
-        vec4 perspective_transform_map(vec4 pos) {
-            vec4 p = $matrix * pos;
-            p = p / p.w;
-            //p.z = 0;
-            p.w = 1;
-            return p;
-        }
-    """
-
-    ## Note 2: Are perspective matrices invertible??
-    #glsl_imap = """
-    #    vec4 perspective_transform_imap(vec4 pos) {
-    #        return $inv_matrix * pos;
-    #    }
-    #"""
-
-    # todo: merge with affinetransform?
-    def set_perspective(self, fov, aspect, near, far):
-        self.matrix = transforms.perspective(fov, aspect, near, far)
-
-    def set_frustum(self, l, r, b, t, n, f):
-        self.matrix = transforms.frustum(l, r, b, t, n, f)
-
-    @arg_to_vec4
-    def map(self, coords):
-        # looks backwards, but both matrices are transposed.
-        v = np.dot(coords, self.matrix)
-        v /= v[:, 3]
-        v[:, 2] = 0
-        return v
-
-    #@arg_to_vec4
-    #def imap(self, coords):
-    #    return np.dot(coords, self.inv_matrix)
-
-    def __mul__(self, tr):
-        # Override multiplication -- this does not combine well with affine
-        # matrices.
-        return tr.__rmul__(self)
diff --git a/vispy/scene/transforms/nonlinear.py b/vispy/scene/transforms/nonlinear.py
deleted file mode 100644
index 9e83b39..0000000
--- a/vispy/scene/transforms/nonlinear.py
+++ /dev/null
@@ -1,162 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from __future__ import division
-
-import numpy as np
-
-from ._util import arg_to_array
-from .base_transform import BaseTransform
-
-
-class LogTransform(BaseTransform):
-    """ Transform perfoming logarithmic transformation on three axes.
-
-    Maps (x, y, z) => (log(base.x, x), log(base.y, y), log(base.z, z))
-
-    No transformation is applied for axes with base == 0.
-
-    If base < 0, then the inverse function is applied: x => base.x ** x
-
-    Parameters
-    ----------
-    base : array-like
-        Base for the X, Y, Z axes.
-    """
-
-    # TODO: Evaluate the performance costs of using conditionals.
-    # An alternative approach is to transpose the vector before
-    # log-transforming, and then transpose back afterward.
-    glsl_map = """
-        vec4 LogTransform_map(vec4 pos) {
-            if($base.x > 1.0)
-                pos.x = log(pos.x) / log($base.x);
-            else if($base.x < -1.0)
-                pos.x = pow(-$base.x, pos.x);
-
-            if($base.y > 1.0)
-                pos.y = log(pos.y) / log($base.y);
-            else if($base.y < -1.0)
-                pos.y = pow(-$base.y, pos.y);
-
-            if($base.z > 1.0)
-                pos.z = log(pos.z) / log($base.z);
-            else if($base.z < -1.0)
-                pos.z = pow(-$base.z, pos.z);
-            return pos;
-        }
-        """
-
-    glsl_imap = glsl_map
-
-    Linear = False
-    Orthogonal = True
-    NonScaling = False
-    Isometric = False
-
-    def __init__(self, base=None):
-        super(LogTransform, self).__init__()
-        self._base = np.zeros(3, dtype=np.float32)
-        self.base = (0.0, 0.0, 0.0) if base is None else base
-
-    @property
-    def base(self):
-        """
-        *base* is a tuple (x, y, z) containing the log base that should be
-        applied to each axis of the input vector. If any axis has a base <= 0,
-        then that axis is not affected.
-        """
-        return self._base.copy()
-
-    @base.setter
-    def base(self, s):
-        self._base[:len(s)] = s
-        self._base[len(s):] = 0.0
-
-    @arg_to_array
-    def map(self, coords, base=None):
-        ret = np.empty(coords.shape, coords.dtype)
-        if base is None:
-            base = self.base
-        for i in range(ret.shape[-1]):
-            if base[i] > 1.0:
-                ret[..., i] = np.log(coords[..., i]) / np.log(base[i])
-            elif base[i] < -1.0:
-                ret[..., i] = -base[i] ** coords[..., i]
-            else:
-                ret[..., i] = coords[..., i]
-        return ret
-
-    @arg_to_array
-    def imap(self, coords):
-        return self.map(coords, -self.base)
-
-    def shader_map(self):
-        fn = super(LogTransform, self).shader_map()
-        fn['base'] = self.base  # uniform vec3
-        return fn
-
-    def shader_imap(self):
-        fn = super(LogTransform, self).shader_imap()
-        fn['base'] = -self.base  # uniform vec3
-        return fn
-
-    def __repr__(self):
-        return "<LogTransform base=%s>" % (self.base)
-
-
-class PolarTransform(BaseTransform):
-    """Polar transform
-
-    Maps (theta, r, z) to (x, y, z), where `x = r*cos(theta)`
-    and `y = r*sin(theta)`.
-    """
-    glsl_map = """
-        vec4 polar_transform_map(vec4 pos) {
-            return vec4(pos.y * cos(pos.x), pos.y * sin(pos.x), pos.z, 1);
-        }
-        """
-
-    glsl_imap = """
-        vec4 polar_transform_map(vec4 pos) {
-            // TODO: need some modulo math to handle larger theta values..?
-            float theta = atan(pos.y, pos.x);
-            float r = length(pos.xy);
-            return vec4(theta, r, pos.z, 1);
-        }
-        """
-
-    Linear = False
-    Orthogonal = False
-    NonScaling = False
-    Isometric = False
-
-    @arg_to_array
-    def map(self, coords):
-        ret = np.empty(coords.shape, coords.dtype)
-        ret[..., 0] = coords[..., 1] * np.cos(coords[..., 0])
-        ret[..., 1] = coords[..., 1] * np.sin(coords[..., 0])
-        for i in range(2, coords.shape[-1]):  # copy any further axes
-            ret[..., i] = coords[..., i]
-        return ret
-
-    @arg_to_array
-    def imap(self, coords):
-        ret = np.empty(coords.shape, coords.dtype)
-        ret[..., 0] = np.arctan2(coords[..., 0], coords[..., 1])
-        ret[..., 1] = (coords[..., 0]**2 + coords[..., 1]**2) ** 0.5
-        for i in range(2, coords.shape[-1]):  # copy any further axes
-            ret[..., i] = coords[..., i]
-        return ret
-
-
-#class BilinearTransform(BaseTransform):
-#    # TODO
-#    pass
-
-
-#class WarpTransform(BaseTransform):
-#    """ Multiple bilinear transforms in a grid arrangement.
-#    """
-#    # TODO
diff --git a/vispy/scene/visuals.py b/vispy/scene/visuals.py
new file mode 100644
index 0000000..195d0fa
--- /dev/null
+++ b/vispy/scene/visuals.py
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+"""
+The classes in scene.visuals are visuals that may be added to a scenegraph
+using the methods and properties defined in `vispy.scene.Node` such as name,
+visible, parent, children, etc...
+
+These classes are automatically generated by mixing `vispy.scene.Node` with
+the Visual classes found in `vispy.visuals`.
+
+For developing custom visuals, it is recommended to subclass from
+`vispy.visuals.Visual` rather than `vispy.scene.Node`.
+"""
+import re
+
+from .. import visuals
+from .node import Node
+
+
+def create_visual_node(subclass):
+    # Create a new subclass of Node.
+    
+    # Decide on new class name
+    clsname = subclass.__name__
+    assert clsname.endswith('Visual')
+    clsname = clsname[:-6]
+    
+    # Generate new docstring based on visual docstring
+    try:
+        doc = generate_docstring(subclass, clsname)
+    except Exception:
+        # If parsing fails, just return the original Visual docstring
+        doc = subclass.__doc__
+    
+    # New __init__ method
+    def __init__(self, *args, **kwargs):
+        parent = kwargs.pop('parent', None)
+        name = kwargs.pop('name', None)
+        self.name = name  # to allow __str__ before Node.__init__
+        subclass.__init__(self, *args, **kwargs)
+        Node.__init__(self, parent=parent, name=name)
+    
+    # Create new class
+    cls = type(clsname, (subclass, Node), {'__init__': __init__, 
+                                           '__doc__': doc})
+    return cls
+
+
+def generate_docstring(subclass, clsname):
+    # Generate a Visual+Node docstring by modifying the Visual's docstring
+    # to include information about Node inheritance and extra init args.
+    
+    sc_doc = subclass.__doc__
+    if sc_doc is None:
+        sc_doc = ""
+        
+    # find locations within docstring to insert new parameters
+    lines = sc_doc.split("\n")
+    
+    # discard blank lines at start
+    while lines and lines[0].strip() == '':
+        lines.pop(0)
+
+    i = 0
+    params_started = False
+    param_indent = None
+    first_blank = None
+    param_end = None
+    while i < len(lines):
+        line = lines[i]
+        # ignore blank lines and '------' lines
+        if re.search(r'\w', line):  
+            indent = len(line) - len(line.lstrip())
+            # If Params section has already started, check for end of params
+            # (that is where we will insert new params)
+            if params_started:
+                if indent < param_indent:
+                    break
+                elif indent == param_indent:
+                    # might be end of parameters block..
+                    if re.match(r'\s*[a-zA-Z0-9_]+\s*:\s*\S+', line) is None:
+                        break
+                param_end = i + 1
+            
+            # Check for beginning of params section
+            elif re.match(r'\s*Parameters\s*', line):
+                params_started = True
+                param_indent = indent
+                if first_blank is None:
+                    first_blank = i
+        
+        # Check for first blank line
+        # (this is where the Node inheritance description will be 
+        # inserted)
+        elif first_blank is None and line.strip() == '':
+            first_blank = i
+
+        i += 1
+        if i == len(lines) and param_end is None:
+            # reached end of docstring; insert here
+            param_end = i
+
+    # If original docstring has no params heading, we need to generate it.
+    if not params_started:
+        lines.extend(["", "    Parameters", "    ----------"])
+        param_end = len(lines)
+        if first_blank is None:
+            first_blank = param_end - 3
+        params_started = True
+    
+    # build class and parameter description strings
+    class_desc = ("\n    This class inherits from visuals.%sVisual and "
+                  "scene.Node, allowing the visual to be placed inside a "
+                  "scenegraph.\n" % (clsname))
+    parm_doc = ("    parent : Node\n"
+                "        The parent node to assign to this node (optional).\n"
+                "    name : string\n"
+                "        A name for this node, used primarily for debugging\n"
+                "        (optional).")
+    
+    # assemble all docstring parts
+    lines = (lines[:first_blank] +
+             [class_desc] +
+             lines[first_blank:param_end] +
+             [parm_doc] +
+             lines[param_end:])
+            
+    doc = '\n'.join(lines)
+    return doc
+
+
+__all__ = []
+
+for obj_name in dir(visuals):
+    obj = getattr(visuals, obj_name)
+    if (isinstance(obj, type) and 
+       issubclass(obj, visuals.Visual) and 
+       obj is not visuals.Visual):
+        cls = create_visual_node(obj)
+        globals()[cls.__name__] = cls
+        __all__.append(cls.__name__)
diff --git a/vispy/scene/visuals/__init__.py b/vispy/scene/visuals/__init__.py
deleted file mode 100644
index d89502d..0000000
--- a/vispy/scene/visuals/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-"""
-The vispy.scene.visuals namespace provides a wide range of visuals.
-A Visual is an Entity that displays something.
-
-Visuals do not have to be used in a scenegraph per se; they can also
-be used stand-alone e.g. from a vispy.app.Canvas, or using Glut.
-
-This module provides a library of drawable objects that are intended to
-encapsulate simple graphic objects such as lines, meshes, points, 2D shapes,
-images, text, etc.
-"""
-
-__all__ = ['Visual', 'Ellipse', 'GridLines', 'Image', 'Line', 'LinePlot',
-           'Markers', 'marker_types', 'Mesh', 'Polygon', 'Rectangle',
-           'RegularPolygon', 'SurfacePlot', 'Text', 'XYZAxis']
-
-from .visual import Visual  # noqa
-from .line import Line  # noqa
-from .markers import Markers, marker_types  # noqa
-from .mesh import Mesh  # noqa
-from .image import Image  # noqa
-from .polygon import Polygon  # noqa
-from .ellipse import Ellipse  # noqa
-from .regular_polygon import RegularPolygon  # noqa
-from .rectangle import Rectangle  # noqa
-from .text import Text  # noqa
-from .gridlines import GridLines  # noqa
-from .surface_plot import SurfacePlot  # noqa
-from .isosurface import Isosurface  # noqa
-from .isocurve import Isocurve  # noqa
-from .xyz_axis import XYZAxis  # noqa
-from .line_plot import LinePlot  # noqa
diff --git a/vispy/scene/visuals/image.py b/vispy/scene/visuals/image.py
deleted file mode 100644
index 13f905b..0000000
--- a/vispy/scene/visuals/image.py
+++ /dev/null
@@ -1,158 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from __future__ import division
-
-import numpy as np
-
-from ... import gloo
-from ..transforms import STTransform, NullTransform
-from .modular_mesh import ModularMesh
-from ..components import (TextureComponent, VertexTextureCoordinateComponent,
-                          TextureCoordinateComponent)
-
-
-class Image(ModularMesh):
-    """Visual subclass displaying an image.
-
-    Parameters
-    ----------
-    data : (height, width, 4) ubyte array
-        Image data.
-    method : str
-        Selects method of rendering image in case of non-linear transforms.
-        Each method produces similar results, but may trade efficiency
-        and accuracy. If the transform is linear, this parameter is ignored
-        and a single quad is drawn around the area of the image.
-
-            * 'subdivide': Image is represented as a grid of triangles with
-              texture coordinates linearly mapped.
-            * 'impostor': Image is represented as a quad covering the entire
-              view, with texture coordinates determined by the transform.
-              This produces the best transformation results, but may be slow.
-
-    grid: tuple (rows, cols)
-        If method='subdivide', this tuple determines the number of rows and
-        columns in the image grid.
-    """
-    def __init__(self, data, method='subdivide', grid=(10, 10), **kwargs):
-        super(Image, self).__init__(**kwargs)
-
-        self._data = None
-
-        # maps from quad coordinates to texture coordinates
-        self._tex_transform = STTransform()
-
-        self._texture = None
-        self._interpolation = 'nearest'
-        self.set_data(data)
-        self.set_gl_options(cull_face=('front_and_back',))
-
-        self.method = method
-        self.grid = grid
-
-    def set_data(self, image=None, **kwds):
-        if image is not None:
-            self._data = image
-            self._texture = None
-        super(Image, self).set_data(**kwds)
-
-    @property
-    def interpolation(self):
-        return self._interpolation
-
-    @interpolation.setter
-    def interpolation(self, interp):
-        self._interpolation = interp
-        self.update()
-
-    @property
-    def size(self):
-        return self._data.shape[:2][::-1]
-
-    def _build_data(self, event):
-        # Construct complete data array with position and optionally color
-        if self.transform.Linear:
-            method = 'subdivide'
-            grid = (1, 1)
-        else:
-            method = self.method
-            grid = self.grid
-
-        # TODO: subdivision and impostor modes should be handled by new
-        # components?
-        if method == 'subdivide':
-            # quads cover area of image as closely as possible
-            w = 1.0 / grid[1]
-            h = 1.0 / grid[0]
-
-            quad = np.array([[0, 0, 0], [w, 0, 0], [w, h, 0],
-                             [0, 0, 0], [w, h, 0], [0, h, 0]],
-                            dtype=np.float32)
-            quads = np.empty((grid[1], grid[0], 6, 3), dtype=np.float32)
-            quads[:] = quad
-
-            mgrid = np.mgrid[0.:grid[1], 0.:grid[0]].transpose(1, 2, 0)
-            mgrid = mgrid[:, :, np.newaxis, :]
-            mgrid[..., 0] *= w
-            mgrid[..., 1] *= h
-
-            quads[..., :2] += mgrid
-            tex_coords = quads.reshape(grid[1]*grid[0]*6, 3)
-            vertices = tex_coords.copy()
-            vertices[..., 0] *= self._data.shape[1]
-            vertices[..., 1] *= self._data.shape[0]
-            ModularMesh.set_data(self, pos=vertices)
-            coords = np.ascontiguousarray(tex_coords[:, :2])
-            tex_coord_comp = TextureCoordinateComponent(coords)
-        elif method == 'impostor':
-            # quad covers entire view; frag. shader will deal with image shape
-            quad = np.array([[-1, -1, 0], [1, -1, 0], [1, 1, 0],
-                             [-1, -1, 0], [1, 1, 0], [-1, 1, 0]],
-                            dtype=np.float32)
-            ModularMesh.set_data(self, pos=quad)
-
-            self._tex_transform.scale = (1./self._data.shape[0],
-                                         1./self._data.shape[1])
-            ctr = event.render_transform.inverse
-            total_transform = self._tex_transform * ctr
-            tex_coord_comp = VertexTextureCoordinateComponent(total_transform)
-            tr = NullTransform().shader_map()
-            self._program.vert['map_local_to_nd'] = tr
-        else:
-            raise ValueError("Unknown image draw method '%s'" % method)
-
-        self._texture = gloo.Texture2D(self._data)
-        self._texture.interpolation = self._interpolation
-
-        self.color_components = [TextureComponent(self._texture,
-                                                  tex_coord_comp)]
-
-    def _activate_transform(self, event=None):
-        # this is handled in _build_data instead.
-        pass
-
-    def bounds(self, mode, axis):
-        if axis > 1:
-            return (0, 0)
-        else:
-            return (0, self.size[axis])
-
-    def draw(self, event):
-        if self._data is None:
-            return
-
-        if self.transform.Linear:
-            method = 'subdivide'
-        else:
-            method = self.method
-
-        # always have to rebuild for impostor, only first for subdivide
-        if self._texture is None or method == 'impostor':
-            self._build_data(event)
-        if method == 'subdivide':
-            tr = event.render_transform.shader_map()
-            self._program.vert['map_local_to_nd'] = tr
-
-        super(Image, self).draw(event)
diff --git a/vispy/scene/visuals/line/line.py b/vispy/scene/visuals/line/line.py
deleted file mode 100644
index 31174a5..0000000
--- a/vispy/scene/visuals/line/line.py
+++ /dev/null
@@ -1,424 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-"""
-Line visual implementing Agg- and GL-based drawing modes.
-"""
-
-from __future__ import division
-
-import numpy as np
-
-from .... import gloo
-from ....color import ColorArray
-from ...shaders import ModularProgram, Function
-from ..visual import Visual
-
-from .dash_atlas import DashAtlas
-from .vertex import VERTEX_SHADER as AGG_VERTEX_SHADER
-from .fragment import FRAGMENT_SHADER as AGG_FRAGMENT_SHADER
-
-
-vec2to4 = Function("""
-    vec4 vec2to4(vec2 input) {
-        return vec4(input, 0, 1);
-    }
-""")
-
-vec3to4 = Function("""
-    vec4 vec3to4(vec3 input) {
-        return vec4(input, 1);
-    }
-""")
-
-
-"""
-TODO:
-
-* Agg support is very minimal; needs attention.
-* Optimization--avoid creating new buffers, avoid triggering program
-  recompile.
-"""
-
-
-joins = {'miter': 0, 'round': 1, 'bevel': 2}
-
-caps = {'': 0, 'none': 0, '.': 0,
-        'round': 1, ')': 1, '(': 1, 'o': 1,
-        'triangle in': 2, '<': 2,
-        'triangle out': 3, '>': 3,
-        'square': 4, '=': 4, 'butt': 4,
-        '|': 5}
-
-_agg_vtype = np.dtype([('a_position', 'f4', 2),
-                       ('a_tangents', 'f4', 4),
-                       ('a_segment',  'f4', 2),
-                       ('a_angles',   'f4', 2),
-                       ('a_texcoord', 'f4', 2),
-                       ('alength', 'f4', 1),
-                       ('color', 'f4', 4)])
-
-
-def _agg_bake(vertices, color, closed=False):
-    """
-    Bake a list of 2D vertices for rendering them as thick line. Each line
-    segment must have its own vertices because of antialias (this means no
-    vertex sharing between two adjacent line segments).
-    """
-
-    n = len(vertices)
-    P = np.array(vertices).reshape(n, 2).astype(float)
-    idx = np.arange(n)  # used to eventually tile the color array
-
-    dx, dy = P[0] - P[-1]
-    d = np.sqrt(dx*dx+dy*dy)
-
-    # If closed, make sure first vertex = last vertex (+/- epsilon=1e-10)
-    if closed and d > 1e-10:
-        P = np.append(P, P[0]).reshape(n+1, 2)
-        idx = np.append(idx, idx[-1])
-        n += 1
-
-    V = np.zeros(len(P), dtype=_agg_vtype)
-    V['a_position'] = P
-
-    # Tangents & norms
-    T = P[1:] - P[:-1]
-
-    N = np.sqrt(T[:, 0]**2 + T[:, 1]**2)
-    # T /= N.reshape(len(T),1)
-    V['a_tangents'][+1:, :2] = T
-    V['a_tangents'][0, :2] = T[-1] if closed else T[0]
-    V['a_tangents'][:-1, 2:] = T
-    V['a_tangents'][-1, 2:] = T[0] if closed else T[-1]
-
-    # Angles
-    T1 = V['a_tangents'][:, :2]
-    T2 = V['a_tangents'][:, 2:]
-    A = np.arctan2(T1[:, 0]*T2[:, 1]-T1[:, 1]*T2[:, 0],
-                   T1[:, 0]*T2[:, 0]+T1[:, 1]*T2[:, 1])
-    V['a_angles'][:-1, 0] = A[:-1]
-    V['a_angles'][:-1, 1] = A[+1:]
-
-    # Segment
-    L = np.cumsum(N)
-    V['a_segment'][+1:, 0] = L
-    V['a_segment'][:-1, 1] = L
-    #V['a_lengths'][:,2] = L[-1]
-
-    # Step 1: A -- B -- C  =>  A -- B, B' -- C
-    V = np.repeat(V, 2, axis=0)[1:-1]
-    V['a_segment'][1:] = V['a_segment'][:-1]
-    V['a_angles'][1:] = V['a_angles'][:-1]
-    V['a_texcoord'][0::2] = -1
-    V['a_texcoord'][1::2] = +1
-    idx = np.repeat(idx, 2)[1:-1]
-
-    # Step 2: A -- B, B' -- C  -> A0/A1 -- B0/B1, B'0/B'1 -- C0/C1
-    V = np.repeat(V, 2, axis=0)
-    V['a_texcoord'][0::2, 1] = -1
-    V['a_texcoord'][1::2, 1] = +1
-    idx = np.repeat(idx, 2)
-
-    I = np.resize(np.array([0, 1, 2, 1, 2, 3], dtype=np.uint32), (n-1)*(2*3))
-    I += np.repeat(4*np.arange(n-1, dtype=np.uint32), 6)
-
-    # Length
-    V['alength'] = L[-1] * np.ones(len(V))
-
-    # Color
-    if color.ndim == 1:
-        color = np.tile(color, (len(V), 1))
-    elif color.ndim == 2 and len(color) == n:
-        color = color[idx]
-    else:
-        raise ValueError('Color length %s does not match number of vertices '
-                         '%s' % (len(color), n))
-    V['color'] = color
-
-    return gloo.VertexBuffer(V), gloo.IndexBuffer(I)
-
-
-GL_VERTEX_SHADER = """
-    varying vec4 v_color;
-
-    void main(void)
-    {
-        gl_Position = $transform($position);
-        v_color = $color;
-    }
-"""
-
-GL_FRAGMENT_SHADER = """
-    varying vec4 v_color;
-    void main()
-    {
-        gl_FragColor = v_color;
-    }
-"""
-
-
-class Line(Visual):
-    """Line visual
-
-    Parameters
-    ----------
-    pos : array
-        Array of shape (..., 2) or (..., 3) specifying vertex coordinates.
-    color : Color, tuple, or array
-        The color to use when drawing the line. If an array is given, it
-        must be of shape (..., 4) and provide one rgba color per vertex.
-    width:
-        The width of the line in px. Line widths > 1px are only
-        guaranteed to work when using 'agg' mode.
-    connect : str or array
-        Determines which vertices are connected by lines.
-            * "strip" causes the line to be drawn with each vertex
-              connected to the next.
-            * "segments" causes each pair of vertices to draw an
-              independent line segment
-            * numpy arrays specify the exact set of segment pairs to
-              connect.
-    mode : str
-        Mode to use for drawing.
-            * "agg" uses anti-grain geometry to draw nicely antialiased lines
-              with proper joins and endcaps.
-            * "gl" uses OpenGL's built-in line rendering. This is much faster,
-              but produces much lower-quality results and is not guaranteed to
-              obey the requested line width or join/endcap styles.
-    antialias : bool 
-        For mode='gl', specifies whether to use line smoothing or not.
-    """
-    def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1,
-                 connect='strip', mode='gl', antialias=False, **kwds):
-        # todo: Get rid of aa argument? It's a bit awkward since ...
-        # - line_smooth is not supported on ES 2.0
-        # - why on earth would you turn off aa with agg?
-        Visual.__init__(self, **kwds)
-        self._pos = pos
-        self._color = ColorArray(color)
-        self._width = float(width)
-        assert connect is not None  # can't be to start
-        self._connect = connect
-        self._mode = 'none'
-        self._origs = {}
-        self.antialias = antialias
-        self._vbo = None
-        self._I = None
-        # Set up the GL program
-        self._gl_program = ModularProgram(GL_VERTEX_SHADER,
-                                          GL_FRAGMENT_SHADER)
-        # Set up the AGG program
-        self._agg_program = ModularProgram(AGG_VERTEX_SHADER,
-                                           AGG_FRAGMENT_SHADER)
-        # agg attributes
-        self._da = None
-        self._U = None
-        self._dash_atlas = None
-        
-        # now actually set the mode, which will call set_data
-        self.mode = mode
-
-    @property
-    def antialias(self):
-        return self._antialias
-
-    @antialias.setter
-    def antialias(self, aa):
-        self._antialias = bool(aa)
-        self.update()
-
-    @property
-    def mode(self):
-        """The current drawing mode"""
-        return self._mode
-
-    @mode.setter
-    def mode(self, mode):
-        if mode not in ('agg', 'gl'):
-            raise ValueError('mode argument must be "agg" or "gl".')
-        if mode == self._mode:
-            return
-        # If the mode changed, reset everything
-        self._mode = mode
-        if self._mode == 'agg' and self._da is None:
-            self._da = DashAtlas()
-            dash_index, dash_period = self._da['solid']
-            self._U = dict(dash_index=dash_index, dash_period=dash_period,
-                           linejoin=joins['round'],
-                           linecaps=(caps['round'], caps['round']),
-                           dash_caps=(caps['round'], caps['round']),
-                           linewidth=self._width, antialias=1.0)
-            self._dash_atlas = gloo.Texture2D(self._da._data)
-            
-        # do not call subclass set_data; this is often overridden with a 
-        # different signature.
-        Line.set_data(self, self._pos, self._color, self._width, self._connect)
-
-    def set_data(self, pos=None, color=None, width=None, connect=None):
-        """ Set the data used to draw this visual.
-
-        Parameters
-        ----------
-        pos : array
-            Array of shape (..., 2) or (..., 3) specifying vertex coordinates.
-        color : Color, tuple, or array
-            The color to use when drawing the line. If an array is given, it
-            must be of shape (..., 4) and provide one rgba color per vertex.
-        width:
-            The width of the line in px. Line widths > 1px are only
-            guaranteed to work when using 'agg' mode.
-        connect : str or array
-            Determines which vertices are connected by lines.
-            * "strip" causes the line to be drawn with each vertex
-              connected to the next.
-            * "segments" causes each pair of vertices to draw an
-              independent line segment
-            * int numpy arrays specify the exact set of segment pairs to
-              connect.
-            * bool numpy arrays specify which _adjacent_ pairs to connect.
-        """
-        if isinstance(connect, np.ndarray) and connect.dtype == bool:
-            connect = self._convert_bool_connect(connect)
-        
-        self._origs = {'pos': pos, 'color': color, 
-                       'width': width, 'connect': connect}
-        
-        if color is not None:
-            self._color = ColorArray(color).rgba
-            if len(self._color) == 1:
-                self._color = self._color[0]
-                
-        if width is not None:
-            self._width = width
-            
-        if self.mode == 'gl':
-            self._gl_set_data(**self._origs)
-        else:
-            self._agg_set_data(**self._origs)
-
-    def _convert_bool_connect(self, connect):
-        # Convert a boolean connection array to a vertex index array
-        assert connect.ndim == 1
-        index = np.empty((len(connect), 2), dtype=np.uint32)
-        index[:] = np.arange(len(connect))[:, np.newaxis]
-        index[:, 1] += 1
-        return index[connect]
-            
-    def _gl_set_data(self, pos, color, width, connect):
-        if connect is not None:
-            if isinstance(connect, np.ndarray):
-                self._connect = gloo.IndexBuffer(connect.astype(np.uint32))
-            else:
-                self._connect = connect
-        if pos is not None:
-            self._pos = pos
-            pos_arr = np.asarray(pos, dtype=np.float32)
-            vbo = gloo.VertexBuffer(pos_arr)
-            if pos_arr.shape[-1] == 2:
-                self._pos_expr = vec2to4(vbo)
-            elif pos_arr.shape[-1] == 3:
-                self._pos_expr = vec3to4(vbo)
-            else:
-                raise TypeError("pos array should have 2 or 3 elements in last"
-                                " axis. shape=%r" % pos_arr.shape)
-            self._vbo = vbo
-        else:
-            self._pos = None
-        self.update()
-
-    def _agg_set_data(self, pos, color, width, connect):
-        if connect is not None:
-            if connect != 'strip':
-                raise NotImplementedError("Only 'strip' connection mode "
-                                          "allowed for agg-mode lines.")
-            self._connect = connect
-        if pos is not None:
-            self._pos = pos
-            self._vbo, self._I = _agg_bake(pos, self._color)
-        else:
-            self._pos = None
-
-        self.update()
-
-    def bounds(self, mode, axis):
-        if 'pos' not in self._origs:
-            return None
-        data = self._origs['pos']
-        if data.shape[1] > axis:
-            return (data[:, axis].min(), data[:, axis].max())
-        else:
-            return (0, 0)
-
-    def draw(self, event):
-        if self.mode == 'gl':
-            self._gl_draw(event)
-        else:
-            self._agg_draw(event)
-
-    def _gl_draw(self, event):
-        if self._pos is None:
-            return
-        xform = event.render_transform.shader_map()
-        self._gl_program.vert['transform'] = xform
-        self._gl_program.vert['position'] = self._pos_expr
-        if self._color.ndim == 1:
-            self._gl_program.vert['color'] = self._color
-        else:
-            self._gl_program.vert['color'] = gloo.VertexBuffer(self._color)
-        gloo.set_state('translucent')
-        
-        # Do we want to use OpenGL, and can we?
-        GL = None
-        if self._width > 1 or self._antialias:
-            try:
-                import OpenGL.GL as GL
-            except ImportError:
-                pass
-        
-        # Turn on line smooth and/or line width
-        if GL:
-            if self._antialias:
-                GL.glEnable(GL.GL_LINE_SMOOTH)
-            if GL and self._width > 1:
-                GL.glLineWidth(self._width)
-        
-        # Draw
-        if self._connect == 'strip':
-            self._gl_program.draw('line_strip')
-        elif self._connect == 'segments':
-            self._gl_program.draw('lines')
-        elif isinstance(self._connect, gloo.IndexBuffer):
-            self._gl_program.draw('lines', self._connect)
-        else:
-            raise ValueError("Invalid line connect mode: %r" % self._connect)
-        
-        # Turn off line smooth and/or line width
-        if GL:
-            if self._antialias:
-                GL.glDisable(GL.GL_LINE_SMOOTH)
-            if GL and self._width > 1:
-                GL.glLineWidth(1)
-
-    def _agg_draw(self, event):
-        if self._pos is None:
-            return
-        gloo.set_state('translucent', depth_test=False)
-        data_doc = event.document_transform()
-        doc_px = event.entity_transform(map_from=event.document_cs,
-                                        map_to=event.framebuffer_cs)
-        px_ndc = event.entity_transform(map_from=event.framebuffer_cs,
-                                        map_to=event.render_cs)
-        vert = self._agg_program.vert
-        vert['doc_px_transform'] = doc_px.shader_map()
-        vert['px_ndc_transform'] = px_ndc.shader_map()
-        vert['transform'] = data_doc.shader_map()
-        self._agg_program.prepare()
-        self._agg_program.bind(self._vbo)
-        uniforms = dict(closed=False, miter_limit=4.0, dash_phase=0.0)
-        for n, v in uniforms.items():
-            self._agg_program[n] = v
-        for n, v in self._U.items():
-            self._agg_program[n] = v
-        self._agg_program['u_dash_atlas'] = self._dash_atlas
-        self._agg_program.draw('triangles', self._I)
diff --git a/vispy/scene/visuals/line_plot.py b/vispy/scene/visuals/line_plot.py
deleted file mode 100644
index 318be4b..0000000
--- a/vispy/scene/visuals/line_plot.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-import numpy as np
-
-from .line import Line
-from .markers import Markers
-from .visual import Visual
-
-
-class LinePlot(Visual):
-    """Visual displaying a plot line with optional markers.
-
-    Parameters
-    ----------
-    *args : array | two arrays
-        Arguments can be passed as (Y,), (X, Y) or (np.array((X, Y))).
-    **kwargs : keyword arguments
-        Keyword arguments to pass on to the Line and Marker visuals.
-        Supported arguments are width, connect, color, edge_color, face_color,
-        and edge_width.
-
-    Examples
-    --------
-    All of these syntaxes will work:
-
-        >>> LinePlot(y_vals)
-        >>> LinePlot(x_vals, y_vals)
-        >>> LinePlot(xy_vals)
-
-    See also
-    --------
-    Line, Markers
-    """
-    _line_kwds = ['width', 'connect', 'color']
-    _marker_kwds = ['edge_color', 'face_color', 'edge_width']
-
-    def __init__(self, *args, **kwds):
-        my_kwds = {}
-        for k in self._line_kwds + self._marker_kwds:
-            if k in kwds:
-                my_kwds[k] = kwds.pop(k)
-
-        Visual.__init__(self, **kwds)
-        self._line = Line()
-        self._markers = Markers()
-
-        self.set_data(*args, **my_kwds)
-
-    def set_data(self, *args, **kwds):
-        args = [np.array(x) for x in args]
-
-        if len(args) == 1:
-            arg = args[0]
-            if arg.ndim == 2:
-                # xy array already provided
-                pos = arg
-            elif arg.ndim == 1:
-                # only y supplied, generate arange x
-                pos = np.empty((len(arg), 2), dtype=np.float32)
-                pos[:, 1] = arg
-                pos[:, 0] = np.arange(len(arg))
-            else:
-                raise TypeError("Invalid argument: array must have ndim "
-                                "<= 2.")
-        elif len(args) == 2:
-            pos = np.concatenate([args[0][:, np.newaxis],
-                                  args[1][:, np.newaxis]], axis=1)
-        else:
-            raise TypeError("Too many positional arguments given (max is 2).")
-
-        # todo: have both sub-visuals share the same buffers.
-        line_kwds = {}
-        for k in self._line_kwds:
-            if k in kwds:
-                line_kwds[k] = kwds.pop(k)
-        self._line.set_data(pos=pos, **line_kwds)
-        marker_kwds = {}
-        for k in self._marker_kwds:
-            if k in kwds:
-                marker_kwds[k] = kwds.pop(k)
-        self._markers.set_data(pos=pos, **marker_kwds)
-        if len(kwds) > 0:
-            raise TypeError("Invalid keyword arguments: %s" % kwds.keys())
-
-    def bounds(self, mode, axis):
-        return self._line.bounds(mode, axis)
-
-    def draw(self, event):
-        for v in self._line, self._markers:
-            event.push_entity(v)
-            try:
-                v.draw(event)
-            finally:
-                event.pop_entity()
diff --git a/vispy/scene/visuals/markers.py b/vispy/scene/visuals/markers.py
deleted file mode 100644
index 6b333e2..0000000
--- a/vispy/scene/visuals/markers.py
+++ /dev/null
@@ -1,334 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-# -----------------------------------------------------------------------------
-"""
-Marker Visual and shader definitions.
-"""
-
-import numpy as np
-
-from ...color import Color
-from ...gloo import set_state, VertexBuffer, _check_valid
-from ..shaders import ModularProgram, Function, Variable
-from .visual import Visual
-
-
-vert = """
-uniform mat4 u_projection;
-uniform float u_antialias;
-
-attribute vec3  a_position;
-attribute vec4  a_fg_color;
-attribute vec4  a_bg_color;
-attribute float a_edgewidth;
-attribute float a_size;
-
-varying vec4 v_fg_color;
-varying vec4 v_bg_color;
-varying float v_edgewidth;
-varying float v_antialias;
-
-void main (void) {
-    $v_size = a_size;
-    v_edgewidth = a_edgewidth;
-    v_antialias = u_antialias;
-    v_fg_color  = a_fg_color;
-    v_bg_color  = a_bg_color;
-    gl_Position = $transform(vec4(a_position,1.0));
-    gl_PointSize = $v_size + 2*(v_edgewidth + 1.5*v_antialias);
-}
-"""
-
-
-frag = """
-varying vec4 v_fg_color;
-varying vec4 v_bg_color;
-varying float v_edgewidth;
-varying float v_antialias;
-
-void main()
-{
-    float size = $v_size +2*(v_edgewidth + 1.5*v_antialias);
-    float t = v_edgewidth/2.0-v_antialias;
-
-    // The marker function needs to be linked with this shader
-    float r = $marker(gl_PointCoord, size);
-
-    float d = abs(r) - t;
-    if( r > (v_edgewidth/2.0+v_antialias))
-    {
-        discard;
-    }
-    else if( d < 0.0 )
-    {
-       gl_FragColor = v_fg_color;
-    }
-    else
-    {
-        float alpha = d/v_antialias;
-        alpha = exp(-alpha*alpha);
-        if (r > 0)
-            gl_FragColor = vec4(v_fg_color.rgb, alpha*v_fg_color.a);
-        else
-            gl_FragColor = mix(v_bg_color, v_fg_color, alpha);
-    }
-}
-"""
-
-
-disc = """
-float disc(vec2 pointcoord, float size)
-{
-    float r = length((pointcoord.xy - vec2(0.5,0.5))*size);
-    r -= $v_size/2;
-    return r;
-}
-"""
-
-
-arrow = """
-float arrow(vec2 pointcoord, float size)
-{
-    float r1 = abs(pointcoord.x -.50)*size +
-               abs(pointcoord.y -.5)*size - $v_size/2;
-    float r2 = abs(pointcoord.x -.25)*size +
-               abs(pointcoord.y -.5)*size - $v_size/2;
-    float r = max(r1,-r2);
-    return r;
-}
-"""
-
-
-ring = """
-float ring(vec2 pointcoord, float size)
-{
-    float r1 = length((pointcoord.xy - vec2(0.5,0.5))*size) - $v_size/2;
-    float r2 = length((pointcoord.xy - vec2(0.5,0.5))*size) - $v_size/4;
-    float r = max(r1,-r2);
-    return r;
-}
-"""
-
-
-clobber = """
-float clobber(vec2 pointcoord, float size)
-{
-    const float PI = 3.14159265358979323846264;
-    const float t1 = -PI/2;
-    const vec2  c1 = 0.2*vec2(cos(t1),sin(t1));
-    const float t2 = t1+2*PI/3;
-    const vec2  c2 = 0.2*vec2(cos(t2),sin(t2));
-    const float t3 = t2+2*PI/3;
-    const vec2  c3 = 0.2*vec2(cos(t3),sin(t3));
-
-    float r1 = length((pointcoord.xy- vec2(0.5,0.5) - c1)*size);
-    r1 -= $v_size/3;
-    float r2 = length((pointcoord.xy- vec2(0.5,0.5) - c2)*size);
-    r2 -= $v_size/3;
-    float r3 = length((pointcoord.xy- vec2(0.5,0.5) - c3)*size);
-    r3 -= $v_size/3;
-    float r = min(min(r1,r2),r3);
-    return r;
-}
-"""
-
-
-square = """
-float square(vec2 pointcoord, float size)
-{
-    float r = max(abs(pointcoord.x -.5)*size, abs(pointcoord.y -.5)*size);
-    r -= $v_size/2;
-    return r;
-}
-"""
-
-
-x_ = """
-float x_(vec2 pointcoord, float size)
-{
-    vec2 rotcoord = vec2((pointcoord.x + pointcoord.y - 1.) / sqrt(2.),
-                         (pointcoord.y - pointcoord.x) / sqrt(2.));
-    float r1 = max(abs(rotcoord.x - 0.25)*size,
-                   abs(rotcoord.x + 0.25)*size);
-    float r2 = max(abs(rotcoord.y - 0.25)*size,
-                   abs(rotcoord.y + 0.25)*size);
-    float r3 = max(abs(rotcoord.x)*size,
-                   abs(rotcoord.y)*size);
-    float r = max(min(r1,r2),r3);
-    r -= $v_size/2;
-    return r;
-}
-"""
-
-
-diamond = """
-float diamond(vec2 pointcoord, float size)
-{
-    float r = abs(pointcoord.x -.5)*size + abs(pointcoord.y -.5)*size;
-    r -= $v_size/2;
-    return r;
-}
-"""
-
-
-vbar = """
-float vbar(vec2 pointcoord, float size)
-{
-    float r1 = max(abs(pointcoord.x - 0.75)*size,
-                   abs(pointcoord.x - 0.25)*size);
-    float r3 = max(abs(pointcoord.x - 0.50)*size,
-                   abs(pointcoord.y - 0.50)*size);
-    float r = max(r1,r3);
-    r -= $v_size/2;
-    return r;
-}
-"""
-
-
-hbar = """
-float hbar(vec2 pointcoord, float size)
-{
-    float r2 = max(abs(pointcoord.y - 0.75)*size,
-                   abs(pointcoord.y - 0.25)*size);
-    float r3 = max(abs(pointcoord.x - 0.50)*size,
-                   abs(pointcoord.y - 0.50)*size);
-    float r = max(r2,r3);
-    r -= $v_size/2;
-    return r;
-}
-"""
-
-
-cross = """
-float cross(vec2 pointcoord, float size)
-{
-    float r1 = max(abs(pointcoord.x - 0.75)*size,
-                   abs(pointcoord.x - 0.25)*size);
-    float r2 = max(abs(pointcoord.y - 0.75)*size,
-                   abs(pointcoord.y - 0.25)*size);
-    float r3 = max(abs(pointcoord.x - 0.50)*size,
-                   abs(pointcoord.y - 0.50)*size);
-    float r = max(min(r1,r2),r3);
-    r -= $v_size/2;
-    return r;
-}
-"""
-
-tailed_arrow = """
-float tailed_arrow(vec2 pointcoord, float size)
-{
-    //arrow_right
-    float r1 = abs(pointcoord.x -.50)*size +
-               abs(pointcoord.y -.5)*size - $v_size/2;
-    float r2 = abs(pointcoord.x -.25)*size +
-               abs(pointcoord.y -.5)*size - $v_size/2;
-    float arrow = max(r1,-r2);
-
-    //hbar
-    float r3 = (abs(pointcoord.y-.5)*2+.3)*$v_size-$v_size/2;
-    float r4 = (pointcoord.x -.775)*size;
-    float r6 = abs(pointcoord.x -.5)*size-$v_size/2;
-    float limit = (pointcoord.x -.5)*size +
-                  abs(pointcoord.y -.5)*size - $v_size/2;
-    float hbar = max(limit,max(max(r3,r4),r6));
-
-    return min(arrow,hbar);
-}
-"""
-
-_marker_dict = {
-    'disc': disc,
-    'arrow': arrow,
-    'ring': ring,
-    'clobber': clobber,
-    'square': square,
-    'diamond': diamond,
-    'vbar': vbar,
-    'hbar': hbar,
-    'cross': cross,
-    'tailed_arrow': tailed_arrow,
-    'x': x_,
-    # aliases
-    'o': disc,
-    '+': cross,
-    's': square,
-    '-': hbar,
-    '|': vbar,
-    '->': tailed_arrow,
-    '>': arrow,
-}
-marker_types = tuple(sorted(list(_marker_dict.keys())))
-
-
-class Markers(Visual):
-    """ Visual displaying marker symbols. 
-    """
-    def __init__(self):
-        self._program = ModularProgram(vert, frag)
-        self._v_size_var = Variable('varying float v_size')
-        self._program.vert['v_size'] = self._v_size_var
-        self._program.frag['v_size'] = self._v_size_var
-        Visual.__init__(self)
-
-    def set_data(self, pos=None, style='o', size=10., edge_width=1.,
-                 edge_color='black', face_color='white'):
-        """ Set the data used to display this visual.
-        
-        Parameters
-        ----------
-        pos : array
-            The array of locations to display each symbol.
-        style : str
-            The style of symbol to draw (see Notes).
-        size : float
-            The symbol size in px.
-        edge_width : float
-            The width of the symbol outline in px.
-        edge_color : Color
-            The color used to draw the symbol outline.
-        face_color : Color
-            The color used to draw the symbol interior.
-            
-        Notes
-        -----
-        
-        Allowed style strings are: disc, arrow, ring, clobber, square, diamond,
-        vbar, hbar, cross, tailed_arrow, and x.
-        """
-        assert (isinstance(pos, np.ndarray) and
-                pos.ndim == 2 and pos.shape[1] in (2, 3))
-        assert edge_width > 0
-        self.set_style(style)
-        edge_color = Color(edge_color).rgba
-        face_color = Color(face_color).rgba
-        n = len(pos)
-        data = np.zeros(n, dtype=[('a_position', np.float32, 3),
-                                  ('a_fg_color', np.float32, 4),
-                                  ('a_bg_color', np.float32, 4),
-                                  ('a_size', np.float32, 1),
-                                  ('a_edgewidth', np.float32, 1)])
-        data['a_fg_color'] = edge_color
-        data['a_bg_color'] = face_color
-        data['a_edgewidth'] = edge_width
-        data['a_position'][:, :pos.shape[1]] = pos
-        data['a_size'] = size
-        self._vbo = VertexBuffer(data)
-
-    def set_style(self, style='o'):
-        _check_valid('style', style, marker_types)
-        self._marker_fun = Function(_marker_dict[style])
-        self._marker_fun['v_size'] = self._v_size_var
-        self._program.frag['marker'] = self._marker_fun
-
-    def draw(self, event=None):
-        set_state(depth_test=False, blend=True, clear_color='white',
-                  blend_func=('src_alpha', 'one_minus_src_alpha'))
-        if event is not None:
-            xform = event.render_transform.shader_map()
-            self._program.vert['transform'] = xform
-        self._program.prepare()
-        self._program['u_antialias'] = 1
-        self._program.bind(self._vbo)
-        self._program.draw('points')
diff --git a/vispy/scene/visuals/mesh.py b/vispy/scene/visuals/mesh.py
deleted file mode 100644
index 09b698e..0000000
--- a/vispy/scene/visuals/mesh.py
+++ /dev/null
@@ -1,224 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-# -----------------------------------------------------------------------------
-
-""" A Mesh Visual that uses the new shader Function.
-"""
-
-from __future__ import division
-
-import numpy as np
-
-from .visual import Visual
-from ..shaders import ModularProgram, Function, Varying
-from ...gloo import VertexBuffer, IndexBuffer, set_state
-from ...geometry import MeshData
-from ...color import Color
-
-## Snippet templates (defined as string to force user to create fresh Function)
-# Consider these stored in a central location in vispy ...
-
-
-vertex_template = """
-
-void main() {
-   gl_Position = $transform($position);
-}
-"""
-
-fragment_template = """
-void main() {
-  gl_FragColor = $color;
-}
-"""
-
-phong_template = """
-vec4 phong_shading(vec4 color) {
-    vec4 o = $transform(vec4(0, 0, 0, 1));
-    vec4 n = $transform(vec4($normal, 1));
-    vec3 norm = normalize((n-o).xyz);
-    vec3 light = normalize($light_dir.xyz);
-    float p = dot(light, norm);
-    p = (p < 0. ? 0. : p);
-    vec4 diffuse = $light_color * p;
-    diffuse.a = 1.0;
-    p = dot(reflect(light, norm), vec3(0,0,1));
-    if (p < 0.0) {
-        p = 0.0;
-    }
-    vec4 specular = $light_color * 5.0 * pow(p, 100.);
-    return color * ($ambient + diffuse) + specular;
-}
-"""
-
-## Functions that can be used as is (don't have template variables)
-# Consider these stored in a central location in vispy ...
-
-vec3to4 = Function("""
-vec4 vec3to4(vec3 xyz) {
-    return vec4(xyz, 1.0);
-}
-""")
-
-vec2to4 = Function("""
-vec4 vec2to4(vec2 xyz) {
-    return vec4(xyz, 0.0, 1.0);
-}
-""")
-
-
-class Mesh(Visual):
-
-    def __init__(self, vertices=None, faces=None, vertex_colors=None,
-                 face_colors=None, color=(0.5, 0.5, 1, 1), meshdata=None,
-                 shading=None, mode='triangles', **kwds):
-        Visual.__init__(self, **kwds)
-        # Create a program
-        self._program = ModularProgram(vertex_template, fragment_template)
-
-        # Define buffers
-        self._vertices = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
-        self._normals = None
-        self._faces = IndexBuffer()
-        self._colors = VertexBuffer(np.zeros((0, 4), dtype=np.float32))
-        self._normals = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
-
-        # Whether to use _faces index
-        self._indexed = None
-
-        # Uniform color
-        self._color = Color(color).rgba
-
-        # primtive mode
-        self._mode = mode
-
-        # varyings
-        self._color_var = Varying('v_color', dtype='vec4')
-        self._normal_var = Varying('v_normal', dtype='vec3')
-
-        # Function for computing phong shading
-        self._phong = None
-
-        # Init
-        self.shading = shading
-        # Note we do not call subclass set_data -- often the signatures
-        # do no match.
-        Mesh.set_data(self, vertices=vertices, faces=faces,
-                      vertex_colors=vertex_colors,
-                      face_colors=face_colors, meshdata=meshdata)
-
-    def set_data(self, vertices=None, faces=None, vertex_colors=None,
-                 face_colors=None, meshdata=None, color=None):
-        if meshdata is not None:
-            self._meshdata = meshdata
-        else:
-            self._meshdata = MeshData(vertices=vertices, faces=faces,
-                                      vertex_colors=vertex_colors,
-                                      face_colors=face_colors)
-        if color is not None:
-            self._color = Color(color).rgba
-        self.mesh_data_changed()
-
-    def mesh_data_changed(self):
-        self._data_changed = True
-        self.update()
-
-    def _update_data(self):
-        md = self._meshdata
-
-        # Update vertex/index buffers
-        if self.shading == 'smooth' and not md.has_face_indexed_data():
-            v = md.vertices()
-            self._vertices.set_data(v, convert=True)
-            self._normals.set_data(md.vertex_normals(), convert=True)
-            self._faces.set_data(md.faces(), convert=True)
-            self._indexed = True
-            if md.has_vertex_color():
-                self._colors.set_data(md.vertex_colors(), convert=True)
-            elif md.has_face_color():
-                self._colors.set_data(md.face_colors(), convert=True)
-            else:
-                self._colors.set_data(np.zeros((0, 4), dtype=np.float32))
-        else:
-            v = md.vertices(indexed='faces')
-            self._vertices.set_data(v, convert=True)
-            if self.shading == 'smooth':
-                normals = md.vertex_normals(indexed='faces')
-                self._normals.set_data(normals, convert=True)
-            elif self.shading == 'flat':
-                normals = md.face_normals(indexed='faces')
-                self._normals.set_data(normals, convert=True)
-            else:
-                self._normals.set_data(np.zeros((0, 3), dtype=np.float32))
-            self._indexed = False
-            if md.has_vertex_color():
-                self._colors.set_data(md.vertex_colors(indexed='faces'), 
-                                      convert=True)
-            elif md.has_face_color():
-                self._colors.set_data(md.face_colors(indexed='faces'), 
-                                      convert=True)
-            else:
-                self._colors.set_data(np.zeros((0, 4), dtype=np.float32))
-
-        # Position input handling
-        if v.shape[-1] == 2:
-            self._program.vert['position'] = vec2to4(self._vertices)
-        elif v.shape[-1] == 3:
-            self._program.vert['position'] = vec3to4(self._vertices)
-        else:
-            raise TypeError("Vertex data must have shape (...,2) or (...,3).")
-
-        # Color input handling
-        colors = self._colors if self._colors.size > 0 else self._color
-        self._program.vert[self._color_var] = colors
-
-        # Shading
-        if self.shading is None:
-            self._program.frag['color'] = self._color_var
-            self._phong = None
-        else:
-            self._phong = Function(phong_template)
-
-            # Normal data comes via vertex shader
-            if self._normals.size > 0:
-                normals = self._normals
-            else:
-                normals = (1., 0., 0.)
-
-            self._program.vert[self._normal_var] = normals
-            self._phong['normal'] = self._normal_var
-
-            # Additional phong proprties
-            self._phong['light_dir'] = (1.0, 1.0, 5.0)
-            self._phong['light_color'] = (1.0, 1.0, 1.0, 1.0)
-            self._phong['ambient'] = (0.3, 0.3, 0.3, 1.0)
-
-            self._program.frag['color'] = self._phong(self._color_var)
-
-    @property
-    def shading(self):
-        """ The shading method used.
-        """
-        return self._shading
-
-    @shading.setter
-    def shading(self, value):
-        assert value in (None, 'flat', 'smooth')
-        self._shading = value
-
-    def draw(self, event):
-        set_state('translucent', depth_test=True, cull_face='front_and_back')
-        if self._data_changed:
-            self._update_data()
-
-        self._program.vert['transform'] = event.render_transform.shader_map()
-        if self._phong is not None:
-            self._phong['transform'] = event.document_transform().shader_map()
-
-        # Draw
-        if self._indexed:
-            self._program.draw(self._mode, self._faces)
-        else:
-            self._program.draw(self._mode)
diff --git a/vispy/scene/visuals/modular_line.py b/vispy/scene/visuals/modular_line.py
deleted file mode 100644
index cb65ecc..0000000
--- a/vispy/scene/visuals/modular_line.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from .modular_visual import ModularVisual
-
-
-class ModularLine(ModularVisual):
-    """
-    Displays multiple line segments.
-    """
-    def __init__(self, parent=None, pos=None, color=None, z=0.0,
-                 mode='line_strip', **kwds):
-        super(ModularLine, self).__init__(parent=parent, **kwds)
-
-        glopts = kwds.pop('gl_options', 'translucent')
-        self.set_gl_options(glopts)
-        glopts = kwds.pop('gl_options', 'translucent')
-        self.set_gl_options(glopts)
-        if mode in ('lines', 'line_strip'):
-            self._primitive = mode
-        else:
-            raise ValueError("Invalid line mode '%s'; must be 'lines' or "
-                             "'line-strip'.")
-
-        if pos is not None or color is not None or z is not None:
-            self.set_data(pos=pos, color=color, z=z)
-
-    def set_data(self, pos=None, **kwds):
-        kwds['index'] = kwds.pop('edges', kwds.get('index', None))
-        kwds.pop('width', 1)  # todo: do something with width
-        super(ModularLine, self).set_data(pos, **kwds)
diff --git a/vispy/scene/visuals/modular_mesh.py b/vispy/scene/visuals/modular_mesh.py
deleted file mode 100644
index 1a1ef81..0000000
--- a/vispy/scene/visuals/modular_mesh.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from __future__ import division
-from .modular_visual import ModularVisual
-
-
-class ModularMesh(ModularVisual):
-    """
-    Displays a 3D triangle mesh.
-    """
-    def __init__(self, gl_options='translucent', faces=None, index=None, 
-                 pos=None, z=0.0, color=None, **kwargs):
-        super(ModularMesh, self).__init__(**kwargs)
-        self.set_gl_options(gl_options)
-        self.set_data(faces=faces, index=index, pos=pos, z=z, color=color)
-
-    def set_data(self, **kwds):
-        kwds['index'] = kwds.pop('faces', kwds.get('index', None))
-        super(ModularMesh, self).set_data(**kwds)
diff --git a/vispy/scene/visuals/modular_point.py b/vispy/scene/visuals/modular_point.py
deleted file mode 100644
index 36b2d3d..0000000
--- a/vispy/scene/visuals/modular_point.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from __future__ import division
-
-from ... import gloo
-from .modular_visual import ModularVisual
-from ..shaders import Function
-
-
-class ModularPoint(ModularVisual):
-    """
-    Displays multiple point sprites.
-    """
-    def __init__(self, pos=None, color=None, **kwargs):
-        super(ModularPoint, self).__init__(**kwargs)
-
-        glopts = kwargs.pop('gl_options', 'translucent')
-        self.set_gl_options(glopts)
-
-        if pos is not None or color is not None:
-            self.set_data(pos=pos, color=color)
-
-        # TODO: turn this into a proper component.
-        code = """
-        void set_point_size() {
-            gl_PointSize = 10.0; //size;
-        }
-        """
-        self._program.vert.add_callback('vert_post_hook', Function(code))
-
-    @property
-    def primitive(self):
-        return gloo.gl.GL_POINTS
-
-    def draw(self, event):
-        # HACK: True OpenGL ES does not need to enable point sprite and does
-        # not define these two constants. Desktop OpenGL needs to enable these
-        # two modes but we do not have these two constants because our GL
-        # namespace pretends to be ES.
-        GL_VERTEX_PROGRAM_POINT_SIZE = 34370
-        GL_POINT_SPRITE = 34913
-        gloo.gl.glEnable(GL_VERTEX_PROGRAM_POINT_SIZE)
-        gloo.gl.glEnable(GL_POINT_SPRITE)
-        super(ModularPoint, self).draw(event)
diff --git a/vispy/scene/visuals/modular_visual.py b/vispy/scene/visuals/modular_visual.py
deleted file mode 100644
index 3e75fc0..0000000
--- a/vispy/scene/visuals/modular_visual.py
+++ /dev/null
@@ -1,353 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from __future__ import division, print_function
-
-import numpy as np
-
-from ... import gloo
-from .visual import Visual
-from ..shaders import ModularProgram, Variable
-from ..components import (VisualComponent, XYPosComponent, XYZPosComponent,
-                          UniformColorComponent, VertexColorComponent)
-
-"""
-  - Should have swappable input component to allow a variety of different
-    vertex inputs:
-        2d attribute + z uniform
-        3d attribute
-        2d attribute + z uniform + index
-        3d attribute + index
-        1d attribute + x/y ranges (surface plot)
-        (and any other custom input component the user might come up with)
-
-  - Should have swappable / chainable fragment components:
-        Per-vertex normals (for smooth surfaces)
-        Per-face normals (for faceted surfaces)
-        Colors per-vertex, per-face
-        Materials - phong, etc.
-        Textures - color, bump map, spec map, etc
-        Wireframe rendering (note this might require vertex modification)
-
-  - Make base shaders look like:
-       vertex_input => vertex_adjustment, transform_to_nd, post_hook
-       color_input => color_adjustment
-
-  - For efficiency, the vertex inputs should allow both pre-index and
-    unindexed arrays. However, many fragment shaders may require pre-indexed
-    arrays. For example, drawing faceted surfaces is not possible with
-    unindexed arrays since the normal vector changes each time a vertex is
-    visited.
-        => this means that input components need a way to convert their data
-           and suggest a different input component (?)
-        => More generally, we need to be able to map out all of the available
-           pathways and choose the most efficient one based on the input
-           data format (to avoid unnecessary conversions) and the requirements
-           of individual components (including indexed/unindexed, whether
-           geometry shaders are available, ...)
-
-  - Fragment shaders that do not need normals should obviously not compute them
-
-  - Some materials require a normal vector, but there may be any number of
-    ways to generate a normal: per-vertex interpolated, per-face, bump maps,
-    etc. This means we need a way for one material to indicate that it requires
-    normals, and a way to tell the component which normal-generating component
-    it should use.
-        => Likewise with colors. In fact, normals and colors are similar enough
-           that they could probably share most of the same machinery..
-
-
-    => Color chain   \
-                      ===>  Material chain
-    => Normal chain  /
-
-    Examples:
-        Color input / filters:
-            uniform color
-            color by vertex, color by face
-            texture color
-            float texture + colormap
-            color by height
-            grid contours
-            wireframe
-
-        Normal input:
-            normal per vertex
-            normal per face
-            texture bump map
-            texture normal map
-
-        Material composition:
-            shaded / facets
-            shaded / smooth
-            phong shading
-
-"""
-            
-
-class ModularVisual(Visual):
-    """
-    Abstract modular visual. This extends Visual by implementing a system
-    of attachable components that change the input and output behaviors of
-    the visual. 
-
-    * A modular GLSL program with a standard set of vertex and
-      fragment shader hooks
-    * A mechanism for adding and removing components
-      that affect the vertex position (pos_components) and fragment
-      color (color_components)
-    * A default draw() method that:
-        * activates each of the attached components
-        * negotiates a buffer mode (pre-indexed or unindexed) supported by
-          all components
-        * Requests an index buffer from components (if needed)
-        * Instructs the program to draw using self.primitive
-    * A simple set_data() method intended to serve as an example for
-      subclasses to follow.
-
-    """
-
-    VERTEX_SHADER = """
-    void main(void) {
-        $local_pos = $local_position();
-        vec4 nd_pos = $map_local_to_nd($local_pos);
-        gl_Position = nd_pos;
-
-        $vert_post_hook();
-    }
-    """
-
-    FRAGMENT_SHADER = """
-    // Fragment shader consists of only a single hook that is usually defined
-    // by a chain of functions, each which sets or modifies the curren
-    // fragment color, or discards it.
-    void main(void) {
-        gl_FragColor = $frag_color();
-    }
-    """
-
-    def __init__(self, **kwargs):
-        Visual.__init__(self, **kwargs)
-        
-        # Dict of {'GL_FLAG': bool} and {'glFunctionName': (args)} 
-        # specifications. By default, these are enabled whenever the Visual 
-        # is drawn. This provides a simple way for the user to customize the
-        # appearance of the Visual. Example:
-        #
-        #     { 'GL_BLEND': True,
-        #       'glBlendFunc': ('GL_SRC_ALPHA', 'GL_ONE') }
-        #
-        self._gl_options = [None, {}]
-
-        self._program = ModularProgram(self.VERTEX_SHADER,
-                                       self.FRAGMENT_SHADER)
-        self._program.changed.connect(self._program_changed)
-        
-        self._program.vert['local_pos'] = Variable('local_pos', 
-                                                   vtype='', dtype='vec4')
-        
-        # Generic chains for attaching post-processing functions
-        self._program.vert.add_chain('local_position')
-        self._program.vert.add_chain('vert_post_hook')
-        self._program.frag.add_chain('frag_color')
-
-        # Components for plugging different types of position and color input.
-        self._pos_components = []
-        #self._color_component = None
-        #self.pos_component = XYZPosComponent()
-        self._color_components = []
-        #self.color_components = [UniformColorComponent()]
-
-        # Primitive, default is GL_TRIANGLES
-        self._primitive = gloo.gl.GL_TRIANGLES
-    
-    @property
-    def primitive(self):
-        """
-        The GL primitive used to draw this visual.
-        """
-        return self._primitive
-
-    @property
-    def vertex_index(self):
-        """
-        Returns the IndexBuffer (or None) that should be used when drawing
-        this Visual.
-        """
-        # TODO: What to do here? How do we decide which component should
-        # generate the index?
-        return self.pos_components[0].index
-
-    def set_data(self, pos=None, index=None, z=0.0, color=None):
-        """
-        Default set_data implementation is only used for a few visuals..
-        *pos* must be array of shape (..., 2) or (..., 3).
-        *z* is only used in the former case.
-        """
-        # select input component based on pos.shape
-        if pos is not None:
-            if pos.shape[-1] == 2:
-                comp = XYPosComponent(xy=pos.astype(np.float32), 
-                                      z=z, index=index)
-                self.pos_components = [comp]
-            elif pos.shape[-1] == 3:
-                comp = XYZPosComponent(pos=pos.astype(np.float32), index=index)
-                self.pos_components = [comp]
-            else:
-                raise Exception("Can't handle position data: %s" % pos)
-
-        if color is not None:
-            if isinstance(color, tuple):
-                self.color_components = [UniformColorComponent(color)]
-            elif isinstance(color, np.ndarray):
-                if color.ndim == 1:
-                    self.color_components = [UniformColorComponent(color)]
-                elif color.ndim > 1:
-                    self.color_components = [VertexColorComponent(color)]
-            else:
-                raise Exception("Can't handle color data: %r" % color)
-
-    def set_gl_options(self, default=-1, **kwds):
-        """
-        Set all GL options for this Visual. Most common arguments are 
-        'translucent', 'opaque', and 'additive'.
-        See gloo.set_state() for more information.
-
-        These options are invoked every time the Visual is drawn.
-        """
-        if default is not -1:
-            self._gl_options[0] = default
-        self._gl_options[1] = kwds
-
-    def update_gl_options(self, default=-1, **kwds):
-        """
-        Update GL options rather than replacing all. See set_gl_options().
-        """
-        if default is not -1:
-            self._gl_options[0] = default
-        self._gl_options.update(kwds)
-
-    def gl_options(self):
-        """
-        Return the GL options in use for this Visual.
-        See set_gl_options().
-        """
-        return self._gl_options[0], self._gl_options[1].copy()
-
-    @property
-    def pos_components(self):
-        return self._pos_components[:]
-
-    @pos_components.setter
-    def pos_components(self, comps):
-        for comp in self._pos_components:
-            try:
-                comp._detach()
-            except:
-                print(comp)
-                raise
-        self._pos_components = comps
-        for comp in self._pos_components:
-            comp._attach(self)
-        self.events.update()
-
-    @property
-    def color_components(self):
-        return self._color_components[:]
-
-    @color_components.setter
-    def color_components(self, comps):
-        for comp in self._color_components:
-            try:
-                comp._detach()
-            except:
-                print(comp)
-                raise
-        self._color_components = comps
-        for comp in self._color_components:
-            comp._attach(self)
-        self.events.update()
-
-    def update(self):
-        """
-        This method is called whenever the Visual must be redrawn.
-
-        """
-        self.events.update()
-
-# no need if we use the drawing system
-#     def on_draw(self, event):
-#         """ when we get a draw event from the scenegraph
-#         """
-#         self._visual.transform = event.viewport_transform
-#         self.draw()
-
-    def draw(self, event):
-        """
-        Draw this visual now.
-
-        The default implementation configures GL flags according to the
-        contents of self._gl_options
-
-        """
-        self._activate_gl_options()
-        mode = self._draw_mode()
-        self._activate_components(mode, event)
-        self._program.draw(self.primitive, self.vertex_index)
-
-    # todo: should this be called "buffer_mode" ?
-    def _draw_mode(self):
-        """
-        Return the mode that should be used to draw this visual
-        (DRAW_PRE_INDEXED or DRAW_UNINDEXED)
-        """
-        modes = set([VisualComponent.DRAW_PRE_INDEXED,
-                     VisualComponent.DRAW_UNINDEXED])
-        for comp in (self._color_components + self.pos_components):
-            modes &= comp.supported_draw_modes
-
-        if len(modes) == 0:
-            for c in self._color_components:
-                print(c, c.supported_draw_modes)
-            raise Exception("Visual cannot draw--no mutually supported "
-                            "draw modes between components.")
-
-        #TODO: pick most efficient draw mode!
-        return list(modes)[0]
-
-    def _activate_gl_options(self):
-        gloo.set_state(self._gl_options[0], **self._gl_options[1])
-
-    def _activate_components(self, mode, event):
-        """
-        This is called immediately before drawing to inform all components
-        that a draw is about to occur and to let them assign program
-        variables.
-        """
-        if len(self._pos_components) == 0:
-            raise Exception("Cannot draw visual %s; no position components"
-                            % self)
-        if len(self._color_components) == 0:
-            raise Exception("Cannot draw visual %s; no color components"
-                            % self)
-        comps = self._pos_components + self._color_components
-        all_comps = set(comps)
-        while len(comps) > 0:
-            comp = comps.pop(0)
-            comps.extend(comp._deps)
-            all_comps |= set(comp._deps)
-
-        self._activate_transform(event)
-        
-        for comp in all_comps:
-            comp.activate(self._program, mode)
-
-    def _activate_transform(self, event):
-        # TODO: this must be optimized.
-        # Allow using as plain visual or in a scenegraph
-        t = event.render_transform.shader_map()
-        self._program.vert['map_local_to_nd'] = t
-
-    def _program_changed(self, event):
-        self.update()
diff --git a/vispy/scene/visuals/polygon.py b/vispy/scene/visuals/polygon.py
deleted file mode 100644
index 969b6ef..0000000
--- a/vispy/scene/visuals/polygon.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-
-"""
-Simple polygon visual based on MeshVisual and LineVisual
-"""
-
-from __future__ import division
-
-import numpy as np
-
-from ... import gloo
-from .visual import Visual
-from .mesh import Mesh
-from .line import Line
-from ...color import Color
-from ...geometry import PolygonData
-
-
-class Polygon(Visual):
-    """
-    Displays a 2D polygon
-
-    Parameters
-    ----------
-    pos : array
-        Set of vertices defining the polygon
-    color : str | tuple | list of colors
-        Fill color of the polygon
-    border_color : str | tuple | list of colors
-        Border color of the polygon
-    """
-    def __init__(self, pos=None, color='black',
-                 border_color=None, **kwds):
-        super(Polygon, self).__init__(**kwds)
-
-        self.mesh = None
-        self.border = None
-        self._pos = pos
-        self._color = Color(color)
-        self._border_color = Color(border_color)
-        self._update()
-        #glopts = kwds.pop('gl_options', 'translucent')
-        #self.set_gl_options(glopts)
-
-    @property
-    def transform(self):
-        """ The transform that maps the local coordinate frame to the
-        coordinate frame of the parent.
-        """
-        return Visual.transform.fget(self)
-
-    @transform.setter
-    def transform(self, tr):
-        Visual.transform.fset(self, tr)
-        if self.mesh is not None:
-            self.mesh.transform = tr
-        if self.border is not None:
-            self.border.transform = tr
-
-    @property
-    def pos(self):
-        """ The vertex position of the polygon.
-        """
-        return self._pos
-
-    @pos.setter
-    def pos(self, pos):
-        self._pos = pos
-        self._update()
-
-    @property
-    def color(self):
-        """ The color of the polygon.
-        """
-        return self._color
-
-    @color.setter
-    def color(self, color):
-        self._color = Color(color)
-        self._update()
-
-    @property
-    def border_color(self):
-        """ The border color of the polygon.
-        """
-        return self._border_color
-
-    @border_color.setter
-    def border_color(self, border_color):
-        self._border_color = Color(border_color)
-        self._update()
-
-    def _update(self):
-        self.data = PolygonData(vertices=np.array(self._pos, dtype=np.float32))
-        if self._pos is not None:
-            pts, tris = self.data.triangulate()
-            self.mesh = Mesh(vertices=pts, faces=tris.astype(np.uint32),
-                             color=self._color.rgba)
-            if not self._border_color.is_blank():
-                # Close border if it is not already.
-                border_pos = self._pos
-                if np.any(border_pos[0] != border_pos[1]):
-                    border_pos = np.concatenate([border_pos, border_pos[:1]], 
-                                                axis=0)
-                self.border = Line(pos=border_pos,
-                                   color=self._border_color.rgba, 
-                                   connect='strip')
-        #self.update()
-
-    def set_gl_options(self, *args, **kwds):
-        self.mesh.set_gl_options(*args, **kwds)
-
-    def update_gl_options(self, *args, **kwds):
-        self.mesh.update_gl_options(*args, **kwds)
-
-    def draw(self, event):
-        if self.mesh is not None:
-            gloo.set_state(polygon_offset_fill=True, 
-                           cull_face='front_and_back')
-            gloo.set_polygon_offset(1, 1)
-            self.mesh.draw(event)
-        if self.border is not None:
-            self.border.draw(event)
diff --git a/vispy/scene/visuals/visual.py b/vispy/scene/visuals/visual.py
deleted file mode 100644
index 2052994..0000000
--- a/vispy/scene/visuals/visual.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-from __future__ import division
-
-from ...util import event
-from ..entity import Entity
-
-"""
-API Issues to work out:
-
-  * Need Visual.bounds() as described here:
-    https://github.com/vispy/vispy/issues/141
-
-"""
-
-
-class Visual(Entity):
-    """
-    Abstract class representing a drawable object.
-
-    At a minimum, Visual subclasses should extend the draw() method. 
-
-    Events:
-
-    update : Event
-        Emitted when the visual has changed and needs to be redrawn.
-    bounds_change : Event
-        Emitted when the bounds of the visual have changed.
-
-    """
-
-    def __init__(self, **kwargs):
-        Entity.__init__(self, **kwargs)
-        
-        # Add event for bounds changing
-        self.events.add(bounds_change=event.Event)
-
-    def _update(self):
-        """
-        This method is called internally whenever the Visual needs to be 
-        redrawn. By default, it emits the update event.
-        """
-        self.events.update()
-
-    def draw(self, event):
-        """
-        Draw this visual now.
-        The default implementation does nothing.
-        
-        This function is called automatically when the visual needs to be drawn
-        as part of a scenegraph, or when calling 
-        ``SceneCanvas.draw_visual(...)``. It is uncommon to call this method 
-        manually.
-        
-        The *event* argument is a SceneDrawEvent instance that provides access 
-        to transforms that the visual
-        may use to determine its relationship to the document coordinate
-        system (which provides physical measurements) and the framebuffer
-        coordinate system (which is necessary for antialiasing calculations). 
-        
-        Vertex transformation can be done either on the CPU using 
-        Transform.map(), or on the GPU using the GLSL functions generated by 
-        Transform.shader_map().
-        """
-        pass
-
-    def bounds(self, axis):
-        """
-        Return the boundaries of this visual along *axis*, which may be 0, 1, 
-        or 2. 
-        
-        This is used primarily to allow automatic ViewBox zoom/pan.
-        By default, this method returns None which indicates the object should 
-        be ignored for automatic zooming along *axis*.
-        
-        A scenegraph may also use this information to cull visuals from the
-        display list.
-        """
-        return None
diff --git a/vispy/scene/widgets/__init__.py b/vispy/scene/widgets/__init__.py
index 50af828..2e70f4c 100644
--- a/vispy/scene/widgets/__init__.py
+++ b/vispy/scene/widgets/__init__.py
@@ -1,13 +1,14 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 The vispy.scene.widgets namespace provides a range of widgets to allow
 user interaction. Widgets are rectangular Visual objects such as buttons
 and sliders.
 """
-__all__ = ['Widget', 'ViewBox', 'Grid']
+__all__ = ['Console', 'Grid', 'ViewBox', 'Widget']
 
+from .console import Console  # noqa
 from .grid import Grid  # noqa
 from .viewbox import ViewBox  # noqa
 from .widget import Widget  # noqa
diff --git a/vispy/scene/widgets/anchor.py b/vispy/scene/widgets/anchor.py
index 871b520..cf00560 100644
--- a/vispy/scene/widgets/anchor.py
+++ b/vispy/scene/widgets/anchor.py
@@ -1,10 +1,10 @@
-from ..entity import Entity
+from ..node import Node
 
 
-class Anchor(Entity):
+class Anchor(Node):
     """
-    Anchor is an entity derives parts of its transform from some other
-    corrdinate system in the scene.
+    Anchor is a node derives parts of its transform from some other
+    coordinate system in the scene.
 
     The purpose is to allow children of an Anchor to draw using a position
     (and optionally rotation) specified by one coordinate system, and scaling/
@@ -17,7 +17,7 @@ class Anchor(Entity):
 
         root = Box()
         view = ViewBox(parent=box)
-        plot = Line(parent=ViewBox)
+        plot = LineVisual(parent=ViewBox)
         anchor = Anchor(parent=root, anchor_to=plot, anchor_pos=(10, 0))
         text = Text(parent=anchor,
                     text="Always points to (10,0) relative to line.")
diff --git a/vispy/scene/widgets/console.py b/vispy/scene/widgets/console.py
new file mode 100644
index 0000000..d8c5180
--- /dev/null
+++ b/vispy/scene/widgets/console.py
@@ -0,0 +1,299 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+""" Fast and failsafe GL console """
+
+# Code translated from glumpy
+
+import numpy as np
+
+from ...visuals.shaders import ModularProgram
+from .widget import Widget
+from ...gloo import VertexBuffer, set_state
+from ...color import Color
+from ...ext.six import string_types
+
+
+# Translated from
+# http://www.piclist.com/tecHREF/datafile/charset/
+#     extractor/charset_extractor.htm
+__font_6x8__ = np.array([
+    (0x00, 0x00, 0x00, 0x00, 0x00, 0x00), (0x10, 0xE3, 0x84, 0x10, 0x01, 0x00),
+    (0x6D, 0xB4, 0x80, 0x00, 0x00, 0x00), (0x00, 0xA7, 0xCA, 0x29, 0xF2, 0x80),
+    (0x20, 0xE4, 0x0C, 0x09, 0xC1, 0x00), (0x65, 0x90, 0x84, 0x21, 0x34, 0xC0),
+    (0x21, 0x45, 0x08, 0x55, 0x23, 0x40), (0x30, 0xC2, 0x00, 0x00, 0x00, 0x00),
+    (0x10, 0x82, 0x08, 0x20, 0x81, 0x00), (0x20, 0x41, 0x04, 0x10, 0x42, 0x00),
+    (0x00, 0xA3, 0x9F, 0x38, 0xA0, 0x00), (0x00, 0x41, 0x1F, 0x10, 0x40, 0x00),
+    (0x00, 0x00, 0x00, 0x00, 0xC3, 0x08), (0x00, 0x00, 0x1F, 0x00, 0x00, 0x00),
+    (0x00, 0x00, 0x00, 0x00, 0xC3, 0x00), (0x00, 0x10, 0x84, 0x21, 0x00, 0x00),
+    (0x39, 0x14, 0xD5, 0x65, 0x13, 0x80), (0x10, 0xC1, 0x04, 0x10, 0x43, 0x80),
+    (0x39, 0x10, 0x46, 0x21, 0x07, 0xC0), (0x39, 0x10, 0x4E, 0x05, 0x13, 0x80),
+    (0x08, 0x62, 0x92, 0x7C, 0x20, 0x80), (0x7D, 0x04, 0x1E, 0x05, 0x13, 0x80),
+    (0x18, 0x84, 0x1E, 0x45, 0x13, 0x80), (0x7C, 0x10, 0x84, 0x20, 0x82, 0x00),
+    (0x39, 0x14, 0x4E, 0x45, 0x13, 0x80), (0x39, 0x14, 0x4F, 0x04, 0x23, 0x00),
+    (0x00, 0x03, 0x0C, 0x00, 0xC3, 0x00), (0x00, 0x03, 0x0C, 0x00, 0xC3, 0x08),
+    (0x08, 0x42, 0x10, 0x20, 0x40, 0x80), (0x00, 0x07, 0xC0, 0x01, 0xF0, 0x00),
+    (0x20, 0x40, 0x81, 0x08, 0x42, 0x00), (0x39, 0x10, 0x46, 0x10, 0x01, 0x00),
+    (0x39, 0x15, 0xD5, 0x5D, 0x03, 0x80), (0x39, 0x14, 0x51, 0x7D, 0x14, 0x40),
+    (0x79, 0x14, 0x5E, 0x45, 0x17, 0x80), (0x39, 0x14, 0x10, 0x41, 0x13, 0x80),
+    (0x79, 0x14, 0x51, 0x45, 0x17, 0x80), (0x7D, 0x04, 0x1E, 0x41, 0x07, 0xC0),
+    (0x7D, 0x04, 0x1E, 0x41, 0x04, 0x00), (0x39, 0x14, 0x17, 0x45, 0x13, 0xC0),
+    (0x45, 0x14, 0x5F, 0x45, 0x14, 0x40), (0x38, 0x41, 0x04, 0x10, 0x43, 0x80),
+    (0x04, 0x10, 0x41, 0x45, 0x13, 0x80), (0x45, 0x25, 0x18, 0x51, 0x24, 0x40),
+    (0x41, 0x04, 0x10, 0x41, 0x07, 0xC0), (0x45, 0xB5, 0x51, 0x45, 0x14, 0x40),
+    (0x45, 0x95, 0x53, 0x45, 0x14, 0x40), (0x39, 0x14, 0x51, 0x45, 0x13, 0x80),
+    (0x79, 0x14, 0x5E, 0x41, 0x04, 0x00), (0x39, 0x14, 0x51, 0x55, 0x23, 0x40),
+    (0x79, 0x14, 0x5E, 0x49, 0x14, 0x40), (0x39, 0x14, 0x0E, 0x05, 0x13, 0x80),
+    (0x7C, 0x41, 0x04, 0x10, 0x41, 0x00), (0x45, 0x14, 0x51, 0x45, 0x13, 0x80),
+    (0x45, 0x14, 0x51, 0x44, 0xA1, 0x00), (0x45, 0x15, 0x55, 0x55, 0x52, 0x80),
+    (0x45, 0x12, 0x84, 0x29, 0x14, 0x40), (0x45, 0x14, 0x4A, 0x10, 0x41, 0x00),
+    (0x78, 0x21, 0x08, 0x41, 0x07, 0x80), (0x38, 0x82, 0x08, 0x20, 0x83, 0x80),
+    (0x01, 0x02, 0x04, 0x08, 0x10, 0x00), (0x38, 0x20, 0x82, 0x08, 0x23, 0x80),
+    (0x10, 0xA4, 0x40, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00, 0x00, 0x3F),
+    (0x30, 0xC1, 0x00, 0x00, 0x00, 0x00), (0x00, 0x03, 0x81, 0x3D, 0x13, 0xC0),
+    (0x41, 0x07, 0x91, 0x45, 0x17, 0x80), (0x00, 0x03, 0x91, 0x41, 0x13, 0x80),
+    (0x04, 0x13, 0xD1, 0x45, 0x13, 0xC0), (0x00, 0x03, 0x91, 0x79, 0x03, 0x80),
+    (0x18, 0x82, 0x1E, 0x20, 0x82, 0x00), (0x00, 0x03, 0xD1, 0x44, 0xF0, 0x4E),
+    (0x41, 0x07, 0x12, 0x49, 0x24, 0x80), (0x10, 0x01, 0x04, 0x10, 0x41, 0x80),
+    (0x08, 0x01, 0x82, 0x08, 0x24, 0x8C), (0x41, 0x04, 0x94, 0x61, 0x44, 0x80),
+    (0x10, 0x41, 0x04, 0x10, 0x41, 0x80), (0x00, 0x06, 0x95, 0x55, 0x14, 0x40),
+    (0x00, 0x07, 0x12, 0x49, 0x24, 0x80), (0x00, 0x03, 0x91, 0x45, 0x13, 0x80),
+    (0x00, 0x07, 0x91, 0x45, 0x17, 0x90), (0x00, 0x03, 0xD1, 0x45, 0x13, 0xC1),
+    (0x00, 0x05, 0x89, 0x20, 0x87, 0x00), (0x00, 0x03, 0x90, 0x38, 0x13, 0x80),
+    (0x00, 0x87, 0x88, 0x20, 0xA1, 0x00), (0x00, 0x04, 0x92, 0x49, 0x62, 0x80),
+    (0x00, 0x04, 0x51, 0x44, 0xA1, 0x00), (0x00, 0x04, 0x51, 0x55, 0xF2, 0x80),
+    (0x00, 0x04, 0x92, 0x31, 0x24, 0x80), (0x00, 0x04, 0x92, 0x48, 0xE1, 0x18),
+    (0x00, 0x07, 0x82, 0x31, 0x07, 0x80), (0x18, 0x82, 0x18, 0x20, 0x81, 0x80),
+    (0x10, 0x41, 0x00, 0x10, 0x41, 0x00), (0x30, 0x20, 0x83, 0x08, 0x23, 0x00),
+    (0x29, 0x40, 0x00, 0x00, 0x00, 0x00), (0x10, 0xE6, 0xD1, 0x45, 0xF0, 0x00)
+], dtype=np.float32)
+
+VERTEX_SHADER = """
+uniform vec2 u_logical_scale;
+uniform float u_physical_scale;
+uniform vec4 u_color;
+uniform vec4 u_origin; 
+
+attribute vec2 a_position;
+attribute vec3 a_bytes_012;
+attribute vec3 a_bytes_345;
+
+varying vec4 v_color;
+varying vec3 v_bytes_012, v_bytes_345;
+
+void main (void)
+{
+    gl_Position = u_origin + vec4(a_position * u_logical_scale, 0., 0.);
+    gl_PointSize = 8.0 * u_physical_scale;
+    v_color = u_color;
+    v_bytes_012 = a_bytes_012;
+    v_bytes_345 = a_bytes_345;
+}
+"""
+
+FRAGMENT_SHADER = """
+float segment(float edge0, float edge1, float x)
+{
+    return step(edge0,x) * (1.0-step(edge1,x));
+}
+
+varying vec4 v_color;
+varying vec3 v_bytes_012, v_bytes_345;
+
+vec4 glyph_color(vec2 uv) {
+    if(uv.x > 5.0 || uv.y > 7.0)
+        return vec4(0, 0, 0, 0);
+    else {
+        float index  = floor( (uv.y*6.0+uv.x)/8.0 );
+        float offset = floor( mod(uv.y*6.0+uv.x,8.0));
+        float byte = segment(0.0,1.0,index) * v_bytes_012.x
+                   + segment(1.0,2.0,index) * v_bytes_012.y
+                   + segment(2.0,3.0,index) * v_bytes_012.z
+                   + segment(3.0,4.0,index) * v_bytes_345.x
+                   + segment(4.0,5.0,index) * v_bytes_345.y
+                   + segment(5.0,6.0,index) * v_bytes_345.z;
+        if( floor(mod(byte / (128.0/pow(2.0,offset)), 2.0)) > 0.0 )
+            return v_color;
+        else
+            return vec4(0, 0, 0, 0);
+    }
+}
+
+void main(void)
+{
+    vec2 loc = gl_PointCoord.xy * 8.0;
+    vec2 uv = floor(loc);
+    // use multi-sampling to make the text look nicer
+    vec2 dxy = 0.25*(abs(dFdx(loc)) + abs(dFdy(loc)));
+    vec4 box = floor(vec4(loc-dxy, loc+dxy));
+    vec4 color = glyph_color(floor(loc)) +
+                 0.25 * glyph_color(box.xy) +
+                 0.25 * glyph_color(box.xw) +
+                 0.25 * glyph_color(box.zy) +
+                 0.25 * glyph_color(box.zw);
+    gl_FragColor = color / 2.;
+}
+"""
+
+
+class Console(Widget):
+    """Fast and failsafe text console
+
+    Parameters
+    ----------
+    text_color : instance of Color
+        Color to use.
+    font_size : float
+        Point size to use.
+    """
+    def __init__(self, text_color='black', font_size=12., **kwargs):
+        # Harcoded because of font above and shader program
+        self.text_color = text_color
+        self.font_size = font_size
+        self._char_width = 6
+        self._char_height = 10
+        self._program = ModularProgram(VERTEX_SHADER, FRAGMENT_SHADER)
+        self._pending_writes = []
+        self._text_lines = []
+        self._col = 0
+        self._current_sizes = (-1,) * 3
+        Widget.__init__(self, **kwargs)
+
+    @property
+    def text_color(self):
+        """The color of the text"""
+        return self._text_color
+
+    @text_color.setter
+    def text_color(self, color):
+        self._text_color = Color(color)
+
+    @property
+    def font_size(self):
+        """The font size (in points) of the text"""
+        return self._font_size
+
+    @font_size.setter
+    def font_size(self, font_size):
+        self._font_size = float(font_size)
+
+    def _resize_buffers(self, font_scale):
+        """Resize buffers only if necessary"""
+        new_sizes = (font_scale,) + self.size
+        if new_sizes == self._current_sizes:  # don't need resize
+            return
+        self._n_rows = int(max(self.size[1] /
+                               (self._char_height * font_scale), 1))
+        self._n_cols = int(max(self.size[0] /
+                               (self._char_width * font_scale), 1))
+        self._bytes_012 = np.zeros((self._n_rows, self._n_cols, 3), np.float32)
+        self._bytes_345 = np.zeros((self._n_rows, self._n_cols, 3), np.float32)
+        pos = np.empty((self._n_rows, self._n_cols, 2), np.float32)
+        C, R = np.meshgrid(np.arange(self._n_cols), np.arange(self._n_rows))
+        # We are in left, top orientation
+        x_off = 4.
+        y_off = 4 - self.size[1] / font_scale
+        pos[..., 0] = x_off + self._char_width * C
+        pos[..., 1] = y_off + self._char_height * R
+        self._position = VertexBuffer(pos)
+
+        # Restore lines
+        for ii, line in enumerate(self._text_lines[:self._n_rows]):
+            self._insert_text_buf(line, ii)
+        self._current_sizes = new_sizes
+
+    def draw(self, event):
+        """Draw the widget
+
+        Parameters
+        ----------
+        event : instance of Event
+            The draw event.
+        """
+        super(Console, self).draw(event)
+        if event is None:
+            raise RuntimeError('Event cannot be None')
+        xform = event.get_full_transform()
+        tr = (event.document_to_framebuffer *
+              event.framebuffer_to_render)
+        logical_scale = np.diff(tr.map(([0, 1], [1, 0])), axis=0)[0, :2]
+        tr = event.document_to_framebuffer
+        log_to_phy = np.mean(np.diff(tr.map(([0, 1], [1, 0])), axis=0)[0, :2])
+        n_pix = (self.font_size / 72.) * 92.  # num of pixels tall
+        # The -2 here is because the char_height has a gap built in
+        font_scale = max(n_pix / float((self._char_height-2)), 1)
+        self._resize_buffers(font_scale)
+        self._do_pending_writes()
+        self._program['u_origin'] = xform.map((0, 0, 0, 1))
+        self._program.prepare()
+        self._program['u_logical_scale'] = font_scale * logical_scale
+        self._program['u_color'] = self.text_color.rgba
+        self._program['u_physical_scale'] = font_scale * log_to_phy
+        self._program['a_position'] = self._position
+        self._program['a_bytes_012'] = VertexBuffer(self._bytes_012)
+        self._program['a_bytes_345'] = VertexBuffer(self._bytes_345)
+        set_state(depth_test=False, blend=True,
+                  blend_func=('src_alpha', 'one_minus_src_alpha'))
+        self._program.draw('points')
+
+    def clear(self):
+        """Clear the console"""
+        if hasattr(self, '_bytes_012'):
+            self._bytes_012.fill(0)
+            self._bytes_345.fill(0)
+        self._text_lines = [] * self._n_rows
+        self._pending_writes = []
+
+    def write(self, text='', wrap=True):
+        """Write text and scroll
+
+        Parameters
+        ----------
+        text : str
+            Text to write. ``''`` can be used for a blank line, as a newline
+            is automatically added to the end of each line.
+        wrap : str
+            If True, long messages will be wrapped to span multiple lines.
+        """
+        # Clear line
+        if not isinstance(text, string_types):
+            raise TypeError('text must be a string')
+        # ensure we only have ASCII chars
+        text = text.encode('utf-8').decode('ascii', errors='replace')
+        self._pending_writes.append((text, wrap))
+        self.update()
+
+    def _do_pending_writes(self):
+        """Do any pending text writes"""
+        for text, wrap in self._pending_writes:
+            # truncate in case of *really* long messages
+            text = text[-self._n_cols*self._n_rows:]
+            text = text.split('\n')
+            text = [t if len(t) > 0 else '' for t in text]
+            nr, nc = self._n_rows, self._n_cols
+            for para in text:
+                para = para[:nc] if not wrap else para
+                lines = [para[ii:(ii+nc)] for ii in range(0, len(para), nc)]
+                lines = [''] if len(lines) == 0 else lines
+                for line in lines:
+                    # Update row and scroll if necessary
+                    self._text_lines.insert(0, line)
+                    self._text_lines = self._text_lines[:nr]
+                    self._bytes_012[1:] = self._bytes_012[:-1]
+                    self._bytes_345[1:] = self._bytes_345[:-1]
+                    self._insert_text_buf(line, 0)
+        self._pending_writes = []
+
+    def _insert_text_buf(self, line, idx):
+        """Insert text into bytes buffers"""
+        self._bytes_012[idx] = 0
+        self._bytes_345[idx] = 0
+        # Crop text if necessary
+        I = np.array([ord(c) - 32 for c in line[:self._n_cols]])
+        I = np.clip(I, 0, len(__font_6x8__)-1)
+        if len(I) > 0:
+            b = __font_6x8__[I]
+            self._bytes_012[idx, :len(I)] = b[:, :3]
+            self._bytes_345[idx, :len(I)] = b[:, 3:]
diff --git a/vispy/scene/widgets/grid.py b/vispy/scene/widgets/grid.py
index 77996fb..55492b1 100644
--- a/vispy/scene/widgets/grid.py
+++ b/vispy/scene/widgets/grid.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
@@ -13,18 +13,82 @@ class Grid(Widget):
     """
     Widget that automatically sets the position and size of child Widgets to
     proportionally divide its internal area into a grid.
+
+    Parameters
+    ----------
+    spacing : int
+        Spacing between widgets.
+    **kwargs : dict
+        Keyword arguments to pass to `Widget`.
     """
-    def __init__(self, **kwds):
+    def __init__(self, spacing=6, **kwargs):
+        from .viewbox import ViewBox
         self._next_cell = [0, 0]  # row, col
         self._cells = {}
         self._grid_widgets = {}
-        self.spacing = 6
-        Widget.__init__(self, **kwds)
-
-    def add_widget(self, widget=None, row=None, col=None, row_span=1, 
+        self.spacing = spacing
+        self._n_added = 0
+        self._default_class = ViewBox  # what to add when __getitem__ is used
+        Widget.__init__(self, **kwargs)
+
+    def __getitem__(self, idxs):
+        """Return an item or create it if the location is available"""
+        if not isinstance(idxs, tuple):
+            idxs = (idxs,)
+        if len(idxs) == 1:
+            idxs = idxs + (slice(None),)
+        elif len(idxs) != 2:
+            raise ValueError('Incorrect index: %s' % (idxs,))
+        lims = np.empty((2, 2), int)
+        for ii, idx in enumerate(idxs):
+            if isinstance(idx, int):
+                idx = slice(idx, idx + 1, None)
+            if not isinstance(idx, slice):
+                raise ValueError('indices must be slices or integers, not %s'
+                                 % (type(idx),))
+            if idx.step is not None and idx.step != 1:
+                raise ValueError('step must be one or None, not %s' % idx.step)
+            start = 0 if idx.start is None else idx.start
+            end = self.grid_size[ii] if idx.stop is None else idx.stop
+            lims[ii] = [start, end]
+        layout = self.layout_array
+        existing = layout[lims[0, 0]:lims[0, 1], lims[1, 0]:lims[1, 1]] + 1
+        if existing.any():
+            existing = set(list(existing.ravel()))
+            ii = list(existing)[0] - 1
+            if len(existing) != 1 or ((layout == ii).sum() !=
+                                      np.prod(np.diff(lims))):
+                raise ValueError('Cannot add widget (collision)')
+            return self._grid_widgets[ii][-1]
+        spans = np.diff(lims)[:, 0]
+        item = self.add_widget(self._default_class(),
+                               row=lims[0, 0], col=lims[1, 0],
+                               row_span=spans[0], col_span=spans[1])
+        return item
+
+    def add_widget(self, widget=None, row=None, col=None, row_span=1,
                    col_span=1):
         """
-        Add a new widget to this grid.
+        Add a new widget to this grid. This will cause other widgets in the
+        grid to be resized to make room for the new widget.
+
+        Parameters
+        ----------
+        widget : Widget
+            The Widget to add
+        row : int
+            The row in which to add the widget (0 is the topmost row)
+        col : int
+            The column in which to add the widget (0 is the leftmost column)
+        row_span : int
+            The number of rows to be occupied by this widget. Default is 1.
+        col_span : int
+            The number of columns to be occupied by this widget. Default is 1.
+
+        Notes
+        -----
+        The widget's parent is automatically set to this grid, and all other
+        parent(s) are removed.
         """
         if row is None:
             row = self._next_cell[0]
@@ -36,44 +100,108 @@ class Grid(Widget):
 
         _row = self._cells.setdefault(row, {})
         _row[col] = widget
-        self._grid_widgets[widget] = row, col, row_span, col_span
-        widget.add_parent(self)
+        self._grid_widgets[self._n_added] = (row, col, row_span, col_span,
+                                             widget)
+        self._n_added += 1
+        widget.parent = self
 
         self._next_cell = [row, col+col_span]
         self._update_child_widgets()
         return widget
 
+    def add_grid(self, row=None, col=None, row_span=1, col_span=1,
+                 **kwargs):
+        """
+        Create a new Grid and add it as a child widget.
+
+        Parameters
+        ----------
+        row : int
+            The row in which to add the widget (0 is the topmost row)
+        col : int
+            The column in which to add the widget (0 is the leftmost column)
+        row_span : int
+            The number of rows to be occupied by this widget. Default is 1.
+        col_span : int
+            The number of columns to be occupied by this widget. Default is 1.
+        **kwargs : dict
+            Keyword arguments to pass to the new `Grid`.
+        """
+        from .grid import Grid
+        grid = Grid(**kwargs)
+        return self.add_widget(grid, row, col, row_span, col_span)
+
+    def add_view(self, row=None, col=None, row_span=1, col_span=1,
+                 **kwargs):
+        """
+        Create a new ViewBox and add it as a child widget.
+
+        Parameters
+        ----------
+        row : int
+            The row in which to add the widget (0 is the topmost row)
+        col : int
+            The column in which to add the widget (0 is the leftmost column)
+        row_span : int
+            The number of rows to be occupied by this widget. Default is 1.
+        col_span : int
+            The number of columns to be occupied by this widget. Default is 1.
+        **kwargs : dict
+            Keyword arguments to pass to `ViewBox`.
+        """
+        from .viewbox import ViewBox
+        view = ViewBox(**kwargs)
+        return self.add_widget(view, row, col, row_span, col_span)
+
     def next_row(self):
         self._next_cell = [self._next_cell[0] + 1, 0]
 
-    def _update_child_widgets(self):
-        # Resize all widgets in this grid to share space.
-        # This logic will need a lot of work..
-
+    @property
+    def grid_size(self):
         rvals = [widget[0]+widget[2] for widget in self._grid_widgets.values()]
         cvals = [widget[1]+widget[3] for widget in self._grid_widgets.values()]
-        if len(rvals) == 0 or len(cvals) == 0:
-            return
+        return max(rvals + [0]), max(cvals + [0])
 
-        nrows = max(rvals)
-        ncols = max(cvals)
+    @property
+    def layout_array(self):
+        locs = -1 * np.ones(self.grid_size, int)
+        for key in self._grid_widgets.keys():
+            r, c, rs, cs = self._grid_widgets[key][:4]
+            locs[r:r + rs, c:c + cs] = key
+        return locs
 
+    def __repr__(self):
+        return (('<Grid at %s:\n' % hex(id(self))) +
+                str(self.layout_array + 1) + '>')
+
+    def _update_child_widgets(self):
+        # Resize all widgets in this grid to share space.
+        # This logic will need a lot of work..
+        n_rows, n_cols = self.grid_size
+        if n_rows == 0:
+            return
         # determine starting/ending position of each row and column
         s2 = self.spacing / 2.
         rect = self.rect.padded(self.padding + self.margin - s2)
-        rows = np.linspace(rect.bottom, rect.top, nrows+1)
+        rows = np.linspace(rect.bottom, rect.top, n_rows+1)
         rowstart = rows[:-1] + s2
         rowend = rows[1:] - s2
-        cols = np.linspace(rect.left, rect.right, ncols+1)
+        cols = np.linspace(rect.left, rect.right, n_cols+1)
         colstart = cols[:-1] + s2
         colend = cols[1:] - s2
 
-        for ch in self._grid_widgets:
-            row, col, rspan, cspan = self._grid_widgets[ch]
+        # snap to pixel boundaries to avoid drawing artifacts
+        colstart = np.round(colstart)
+        colend = np.round(colend)
+        rowstart = np.round(rowstart)
+        rowend = np.round(rowend)
+
+        for key in self._grid_widgets.keys():
+            row, col, rspan, cspan, ch = self._grid_widgets[key]
 
-            # Translate the origin of the entity to the corner of the area
-            #ch.transform.reset()
-            #ch.transform.translate((colstart[col], rowstart[row]))
+            # Translate the origin of the node to the corner of the area
+            # ch.transform.reset()
+            # ch.transform.translate((colstart[col], rowstart[row]))
             ch.pos = colstart[col], rowstart[row]
 
             # ..and set the size to match.
diff --git a/vispy/scene/widgets/viewbox.py b/vispy/scene/widgets/viewbox.py
index 0107bae..23e5e75 100644
--- a/vispy/scene/widgets/viewbox.py
+++ b/vispy/scene/widgets/viewbox.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
@@ -10,62 +10,69 @@ from .widget import Widget
 from ..subscene import SubScene
 from ..cameras import make_camera, BaseCamera
 from ...ext.six import string_types
-from ... color import Color
 from ... import gloo
+from ...visuals.components import Clipper
+from ...visuals import Visual
 
 
 class ViewBox(Widget):
     """ Provides a rectangular widget to which its subscene is rendered.
-    
+
     Three classes work together when using a ViewBox:
     * The :class:`SubScene` class describes a "world" coordinate system and the
-    entities that live inside it. 
+    entities that live inside it.
     * ViewBox is a "window" through which we view the
     subscene. Multiple ViewBoxes may view the same subscene.
-    * :class:`Camera` describes both the perspective from which the 
-    subscene is rendered, and the way user interaction affects that 
-    perspective. 
-    
-    In general it is only necessary to create the ViewBox; a SubScene and 
+    * :class:`Camera` describes both the perspective from which the
+    subscene is rendered, and the way user interaction affects that
+    perspective.
+
+    In general it is only necessary to create the ViewBox; a SubScene and
     Camera will be generated automatically.
-    
+
     Parameters
     ----------
     camera : None, :class:`Camera`, or str
-        The camera through which to view the SubScene. If None, then a 
+        The camera through which to view the SubScene. If None, then a
         PanZoomCamera (2D interaction) is used. If str, then the string is
         used as the argument to :func:`make_camera`.
     scene : None or :class:`SubScene`
-        The :class:`SubScene` instance to view. If None, a new 
-        :class:`SubScene` is created.
-    
-    All extra keyword arguments are passed to :func:`Widget.__init__`.
+        The `SubScene` instance to view. If None, a new `SubScene` is created.
+    clip_method : str
+        Clipping method to use.
+    **kwargs : dict
+        Extra keyword arguments to pass to `Widget`.
     """
-    def __init__(self, camera=None, scene=None, bgcolor='black', **kwds):
-        
+    def __init__(self, camera=None, scene=None, clip_method='fragment',
+                 **kwargs):
+
         self._camera = None
-        self._bgcolor = Color(bgcolor).rgba
-        Widget.__init__(self, **kwds)
+        Widget.__init__(self, **kwargs)
 
-        # Init preferred method to provided a pixel grid
-        self._clip_method = 'viewport'
+        # Init method to provide a pixel grid
+        self._clip_method = clip_method
+        self._clipper = Clipper()
 
         # Each viewbox has a scene widget, which has a transform that
         # represents the transformation imposed by camera.
         if scene is None:
-            self._scene = SubScene()
+            if self.name is not None:
+                name = str(self.name) + "_Scene"
+            else:
+                name = None
+            self._scene = SubScene(name=name)
         elif isinstance(scene, SubScene):
             self._scene = scene
         else:
             raise TypeError('Argument "scene" must be None or SubScene.')
         self._scene.add_parent(self)
-        
+
         # Camera is a helper object that handles scene transformation
         # and user interaction.
         if camera is None:
-            camera = 'panzoom'
+            camera = 'base'
         if isinstance(camera, string_types):
-            self.camera = make_camera(camera)
+            self.camera = make_camera(camera, parent=self.scene)
         elif isinstance(camera, BaseCamera):
             self.camera = camera
         else:
@@ -73,38 +80,130 @@ class ViewBox(Widget):
 
     @property
     def camera(self):
-        """ The Camera in use by this ViewBox. 
+        """ Get/set the Camera in use by this ViewBox
+
+        If a string is given (e.g. 'panzoom', 'turntable', 'fly'). A
+        corresponding camera is selected if it already exists in the
+        scene, otherwise a new camera is created.
+
+        The camera object is made a child of the scene (if it is not
+        already in the scene).
+
+        Multiple cameras can exist in one scene, although only one can
+        be active at a time. A single camera can be used by multiple
+        viewboxes at the same time.
         """
         return self._camera
-    
+
     @camera.setter
     def camera(self, cam):
-        if self._camera is not None:
-            self._camera.viewbox = None
-        self._camera = cam
-        cam.viewbox = self
+        if isinstance(cam, string_types):
+            # Try to select an existing camera
+            for child in self.scene.children:
+                if isinstance(child, BaseCamera):
+                    this_cam_type = child.__class__.__name__.lower()[:-6]
+                    if this_cam_type == cam:
+                        self.camera = child
+                        return
+            else:
+                # No such camera yet, create it then
+                self.camera = make_camera(cam)
+            
+        elif isinstance(cam, BaseCamera):
+            # Ensure that the camera is in the scene
+            if not self.is_in_scene(cam):
+                cam.add_parent(self.scene)
+            # Disconnect / connect
+            if self._camera is not None:
+                self._camera._viewbox_unset(self)
+            self._camera = cam
+            if self._camera is not None:
+                self._camera._viewbox_set(self)
+            # Update view
+            cam.view_changed()
         
-    def set_camera(self, cam_type, *args, **kwds):
-        """ Create a new Camera and attach it to this ViewBox. 
+        else:
+            raise ValueError('Not a camera object.')
+
+    def is_in_scene(self, node):
+        """Get whether the given node is inside the scene of this viewbox.
+
+        Parameters
+        ----------
+        node : instance of Node
+            The node.
+        """
+        def _is_child(parent, child):
+            if child in parent.children:
+                return True
+            else:
+                for c in parent.children:
+                    if isinstance(c, ViewBox):
+                        continue
+                    elif _is_child(c, child):
+                        return True
+            return False
         
-        See :func:`make_camera` for arguments.
+        return _is_child(self.scene, node)
+    
+    def get_scene_bounds(self, dim=None):
+        """Get the total bounds based on the visuals present in the scene
+
+        Parameters
+        ----------
+        dim : int | None
+            Dimension to return.
+
+        Returns
+        -------
+        bounds : list | tuple
+            If ``dim is None``, Returns a list of 3 tuples, otherwise
+            the bounds for the requested dimension.
         """
-        self.camera = make_camera(cam_type, *args, **kwds)
-        return self.camera
-
+        # todo: handle sub-children
+        # todo: handle transformations
+        # Init
+        mode = 'data'  # or visual?
+        bounds = [(np.inf, -np.inf), (np.inf, -np.inf), (np.inf, -np.inf)]
+        # Get bounds of all children
+        for ob in self.scene.children:
+            if hasattr(ob, 'bounds'):
+                for axis in (0, 1, 2):
+                    if (dim is not None) and dim != axis:
+                        continue
+                    b = ob.bounds(mode, axis)
+                    if b is not None:
+                        b = min(b), max(b)  # Ensure correct order
+                        bounds[axis] = (min(bounds[axis][0], b[0]), 
+                                        max(bounds[axis][1], b[1]))
+        # Set defaults
+        for axis in (0, 1, 2):
+            if any(np.isinf(bounds[axis])):
+                bounds[axis] = -1, 1
+        
+        if dim is not None:
+            return bounds[dim]
+        else:
+            return bounds
+    
     @property
     def scene(self):
-        """ The root entity of the scene viewed by this ViewBox.
+        """ The root node of the scene viewed by this ViewBox.
         """
         return self._scene
 
-    def add(self, entity):
-        """ Add an Entity to the scene for this ViewBox. 
-        
-        This is a convenience method equivalent to 
-        `entity.add_parent(viewbox.scene)`
+    def add(self, node):
+        """ Add an Node to the scene for this ViewBox.
+
+        This is a convenience method equivalent to
+        `node.add_parent(viewbox.scene)`
+
+        Parameters
+        ----------
+        node : instance of Node
+            The node to add.
         """
-        entity.add_parent(self.scene)
+        node.add_parent(self.scene)
 
     @property
     def clip_method(self):
@@ -118,7 +217,7 @@ class ViewBox(Widget):
           onto the parent pixel grid, if possible.
         * 'fbo' - use an FBO to draw the subscene to a texture, and
           then render the texture in the parent scene.
-        * 'fragment' - clipping in the fragment shader TODO
+        * 'fragment' - clipping in the fragment shader
         * 'stencil' - TODO
 
         Notes
@@ -146,84 +245,108 @@ class ViewBox(Widget):
             t = 'clip_method should be in %s' % str(valid_methods)
             raise ValueError((t + ', not %r') % value)
         self._clip_method = value
+        self.update()
 
     def draw(self, event):
-        """ Draw the viewbox border/background, and prepare to draw the 
+        """ Draw the viewbox border/background
+
+        This also prepares to draw the
         subscene using the configured clipping method.
+
+        Parameters
+        ----------
+        event : instance of Event
+            The draw event.
         """
-        # todo: we could consider including some padding
-        # so that we have room *inside* the viewbox to draw ticks and stuff
-        
         # -- Calculate resolution
-        
+
         # Get current transform and calculate the 'scale' of the viewbox
         size = self.size
-        transform = event.document_transform()
+        transform = event.visual_to_document
         p0, p1 = transform.map((0, 0)), transform.map(size)
         res = (p1 - p0)[:2]
         res = abs(res[0]), abs(res[1])
 
         # Set resolution (note that resolution can be non-integer)
         self._resolution = res
-        
-        # -- Get user clipping preference
 
-        prefer = self.clip_method
+        method = self.clip_method
         viewport, fbo = None, None
 
-        if prefer is None:
-            pass
-        elif prefer == 'fragment':
-            raise NotImplementedError('No fragment shader clipping yet.')
-        elif prefer == 'stencil':
+        if method == 'fragment':
+            self._prepare_fragment()
+        elif method == 'stencil':
             raise NotImplementedError('No stencil buffer clipping yet.')
-        elif prefer == 'viewport':
+        elif method == 'viewport':
             viewport = self._prepare_viewport(event)
-        elif prefer == 'fbo':
+        elif method == 'fbo':
             fbo = self._prepare_fbo(event)
+        else:
+            raise ValueError('Unknown clipping method %s' % method)
 
         # -- Draw
         super(ViewBox, self).draw(event)
 
         event.push_viewbox(self)
 
+        # make sure the current drawing system does not attempt to draw
+        # the scene.
+        event.handled_children.append(self.scene)
         if fbo:
+            canvas_transform = event.visual_to_canvas
+            offset = canvas_transform.map((0, 0))[:2]
+            size = canvas_transform.map(self.size)[:2] - offset
+
             # Ask the canvas to activate the new FBO
-            offset = event.canvas_transform().map((0, 0))[:2]
-            size = event.canvas_transform().map(self.size)[:2] - offset
-            
-            # Draw subscene to FBO
             event.push_fbo(fbo, offset, size)
-            event.push_entity(self.scene)
+            event.push_node(self.scene)
             try:
-                gloo.clear(color=self._bgcolor, depth=True)
+                # Draw subscene to FBO
                 self.scene.draw(event)
             finally:
-                event.pop_entity()
+                event.pop_node()
                 event.pop_fbo()
-            
+
             gloo.set_state(cull_face=False)
-            self._myprogram.draw('triangle_strip')
         elif viewport:
             # Push viewport, draw, pop it
             event.push_viewport(viewport)
-            event.push_entity(self.scene)
+            event.push_node(self.scene)
             try:
                 self.scene.draw(event)
             finally:
-                event.pop_entity()
+                event.pop_node()
                 event.pop_viewport()
-
-        else:
-            # Just draw
-            # todo: invoke fragment shader clipping
-            self.scene.draw(event)
+        elif method == 'fragment':
+            self._clipper.bounds = event.visual_to_framebuffer.map(self.rect)
+            event.push_node(self.scene)
+            try:
+                self.scene.draw(event)
+            finally:
+                event.pop_node()
 
         event.pop_viewbox()
 
+    def _prepare_fragment(self, root=None):
+        # Todo: should only be run when there are changes in the graph, not
+        # on every frame.
+        if root is None:
+            root = self.scene
+
+        for ch in root.children:
+            if isinstance(ch, Visual):
+                try:
+                    ch.attach(self._clipper)
+                except NotImplementedError:
+                    # visual does not support clipping
+                    pass
+            self._prepare_fragment(ch)
+
     def _prepare_viewport(self, event):
-        p1 = event.map_to_framebuffer((0, 0))
-        p2 = event.map_to_framebuffer(self.size)
+        fb_transform = event.node_transform(map_from=event.node_cs,
+                                            map_to=event.framebuffer_cs)
+        p1 = fb_transform.map((0, 0))
+        p2 = fb_transform.map(self.size)
         return p1[0], p1[1], p2[0]-p1[0], p2[1]-p1[1]
 
     def _prepare_fbo(self, event):
@@ -243,9 +366,6 @@ class ViewBox(Widget):
         because I do not understand the component system yet.
         """
 
-        from vispy.gloo import gl
-        from vispy import gloo
-
         render_vertex = """
             attribute vec3 a_position;
             attribute vec2 a_texcoord;
@@ -270,11 +390,10 @@ class ViewBox(Widget):
         # todo: don't do this on every draw
         if True:
             # Create program
-            self._myprogram = gloo.Program(render_vertex, render_fragment)
+            self._fboprogram = gloo.Program(render_vertex, render_fragment)
             # Create texture
-            self._tex = gloo.Texture2D(shape=(10, 10, 4), dtype=np.uint8)
-            self._tex.interpolation = gl.GL_LINEAR
-            self._myprogram['u_texture'] = self._tex
+            self._tex = gloo.Texture2D((10, 10, 4), interpolation='linear')
+            self._fboprogram['u_texture'] = self._tex
             # Create texcoords and vertices
             # Note y-axis is inverted here because the viewbox coordinate
             # system has origin in the upper-left, but the image is rendered
@@ -282,8 +401,8 @@ class ViewBox(Widget):
             texcoord = np.array([[0, 1], [1, 1], [0, 0], [1, 0]],
                                 dtype=np.float32)
             position = np.zeros((4, 3), np.float32)
-            self._myprogram['a_texcoord'] = gloo.VertexBuffer(texcoord)
-            self._myprogram['a_position'] = self._vert = \
+            self._fboprogram['a_texcoord'] = gloo.VertexBuffer(texcoord)
+            self._fboprogram['a_position'] = self._vert = \
                 gloo.VertexBuffer(position)
 
         # Get fbo, ensure it exists
@@ -291,14 +410,13 @@ class ViewBox(Widget):
         if True:  # fbo is None:
             self._fbo = 4
             self._fbo = fbo = gloo.FrameBuffer(self._tex,
-                                               depth=gloo.DepthBuffer((10,
-                                                                       10)))
+                                               gloo.RenderBuffer((10, 10)))
 
         # Set texture coords to make the texture be drawn in the right place
         # Note that we would just use -1..1 if we would use a Visual.
         coords = [[0, 0], [self.size[0], self.size[1]]]
         
-        transform = event.render_transform  # * self.scene.viewbox_transform
+        transform = event.get_full_transform()
         coords = transform.map(coords)
         x1, y1, z = coords[0][:3]
         x2, y2, z = coords[1][:3]
@@ -310,6 +428,7 @@ class ViewBox(Widget):
         # Set fbo size (mind that this is set using shape!)
         resolution = [int(i+0.5) for i in self._resolution]  # set in draw()
         shape = resolution[1], resolution[0]
+        # todo: use fbo.resize(shape)
         fbo.color_buffer.resize(shape+(4,))
         fbo.depth_buffer.resize(shape)
 
diff --git a/vispy/scene/widgets/widget.py b/vispy/scene/widgets/widget.py
index bd925cb..17b440a 100644
--- a/vispy/scene/widgets/widget.py
+++ b/vispy/scene/widgets/widget.py
@@ -1,26 +1,26 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
 
 import numpy as np
 
-from ..visuals.visual import Visual
-from ..visuals.line import Line
-from ..transforms import STTransform
+from ..node import Node
+from ...visuals.mesh import MeshVisual
+from ...visuals.transforms import STTransform
 from ...util.event import Event
 from ...geometry import Rect
 from ...color import Color
 
 
-class Widget(Visual):
+class Widget(Node):
     """ A widget takes up a rectangular space, intended for use in
     a 2D pixel coordinate frame.
 
     The widget is positioned using the transform attribute (as any
-    entity), and its extend (size) is kept as a separate property.
-    
+    node), and its extent (size) is kept as a separate property.
+
     Parameters
     ----------
     pos : (x, y)
@@ -29,6 +29,8 @@ class Widget(Visual):
         A 2-element tuple to spicify the size of the widget.
     border_color : color
         The color of the border.
+    bgcolor : color
+        The background color.
     clip : bool
         Not used :)
     padding : int
@@ -36,24 +38,30 @@ class Widget(Visual):
         the contents and the border).
     margin : int
         The margin to keep outside the widget's border.
-    
     """
 
-    def __init__(self, pos=(0, 0), size=(10, 10), border_color='black',
-                 clip=False, padding=0, margin=0, **kwargs):
-        Visual.__init__(self, **kwargs)
-        
-        # todo: rename to bordercolor? -> borderwidth
-        self._border_color = tuple(Color(border_color).rgba)
-        # for drawing border
-        self._visual = Line(color=self._border_color, mode='gl')
+    def __init__(self, pos=(0, 0), size=(10, 10), border_color=None,
+                 bgcolor=None, clip=False, padding=0, margin=0, **kwargs):
+        Node.__init__(self, **kwargs)
+
+        # For drawing border.
+        # A mesh is required because GL lines cannot be drawn with predictable
+        # shape across all platforms.
+        self._border_color = self._bgcolor = Color(None)
+        self._face_colors = None
+        self._visual = MeshVisual(mode='triangles')
+        self._visual.set_gl_state('translucent', depth_test=False)
+
         # whether this widget should clip its children
+        # (todo)
         self._clip = clip
+
         # reserved space inside border
         self._padding = padding
+
         # reserved space outside border
         self._margin = margin
-        
+
         self.events.add(resize=Event)
         self._size = 16, 16
         self.transform = STTransform()
@@ -62,6 +70,8 @@ class Widget(Visual):
         self._widgets = []
         self.pos = pos
         self.size = size
+        self.border_color = border_color
+        self.bgcolor = bgcolor
 
     @property
     def pos(self):
@@ -108,6 +118,18 @@ class Widget(Visual):
         self.events.resize()
 
     @property
+    def inner_rect(self):
+        """The rectangular area inside the margin, border and padding.
+
+        Generally widgets should avoid drawing or placing widgets outside this
+        rectangle.
+        """
+        m = self.margin + self.padding
+        if not self.border_color.is_blank:
+            m += 1
+        return Rect((m, m), (self.size[0]-2*m, self.size[1]-2*m))
+
+    @property
     def border_color(self):
         """ The color of the border.
         """
@@ -115,8 +137,9 @@ class Widget(Visual):
 
     @border_color.setter
     def border_color(self, b):
-        self._border_color = b
-        self._visual.set_data(color=b)
+        self._border_color = Color(b)
+        self._update_colors()
+        self._update_line()
         self.update()
 
     @property
@@ -128,6 +151,8 @@ class Widget(Visual):
     @bgcolor.setter
     def bgcolor(self, value):
         self._bgcolor = Color(value)
+        self._update_colors()
+        self._update_line()
         self.update()
 
     @property
@@ -146,29 +171,78 @@ class Widget(Visual):
     @padding.setter
     def padding(self, p):
         self._padding = p
-        self._update_child_boxes()
+        self._update_child_widgets()
 
     def _update_line(self):
         """ Update border line to match new shape """
-        if self.border_color is None:
-            return
-        m = self.margin
-        r = self.size[0] - m
-        t = self.size[1] - m
-        
+        w = 1  # XXX Eventually this can be a parameter
+        m = int(self.margin)
+        # border is drawn within the boundaries of the widget:
+        #
+        #  size = (8, 7)  margin=2
+        #  internal rect = (3, 3, 2, 1)
+        #  ........
+        #  ........
+        #  ..BBBB..
+        #  ..B  B..
+        #  ..BBBB..
+        #  ........
+        #  ........
+        #
+        l = b = m
+        r = int(self.size[0]) - m
+        t = int(self.size[1]) - m
         pos = np.array([
-            [m, m],
-            [r, m],
-            [r, t],
-            [m, t],
-            [m, m]]).astype(np.float32)
-        self._visual.set_data(pos=pos)
+            [l, b], [l+w, b+w],
+            [r, b], [r-w, b+w],
+            [r, t], [r-w, t-w],
+            [l, t], [l+w, t-w],
+        ], dtype=np.float32)
+        faces = np.array([
+            [0, 2, 1],
+            [1, 2, 3],
+            [2, 4, 3],
+            [3, 5, 4],
+            [4, 5, 6],
+            [5, 7, 6],
+            [6, 0, 7],
+            [7, 0, 1],
+            [5, 3, 1],
+            [1, 5, 7],
+        ], dtype=np.int32)
+        start = 8 if self._border_color.is_blank else 0
+        stop = 8 if self._bgcolor.is_blank else 10
+        face_colors = None
+        if self._face_colors is not None:
+            face_colors = self._face_colors[start:stop]
+        self._visual.set_data(vertices=pos, faces=faces[start:stop],
+                              face_colors=face_colors)
+
+    def _update_colors(self):
+        self._face_colors = np.concatenate(
+            (np.tile(self.border_color.rgba, (8, 1)),
+             np.tile(self.bgcolor.rgba, (2, 1)))).astype(np.float32)
 
     def draw(self, event):
-        if self.border_color is not None:
-            self._visual.draw(event)
+        """Draw the widget borders
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event containing the transforms.
+        """
+        if self.border_color.is_blank and self.bgcolor.is_blank:
+            return
+        self._visual.draw(event)
+
+    def on_resize(self, event):
+        """On resize handler
 
-    def on_resize(self, ev):
+        Parameters
+        ----------
+        event : instance of Event
+            The resize event.
+        """
         self._update_child_widgets()
 
     def _update_child_widgets(self):
@@ -179,36 +253,56 @@ class Widget(Visual):
 
     def add_widget(self, widget):
         """
-        Add a Widget as a managed child of this Widget. The child will be
+        Add a Widget as a managed child of this Widget.
+
+        The child will be
         automatically positioned and sized to fill the entire space inside
         this Widget (unless _update_child_widgets is redefined).
+
+        Parameters
+        ----------
+        widget : instance of Widget
+            The widget to add.
+
+        Returns
+        -------
+        widget : instance of Widget
+            The widget.
         """
         self._widgets.append(widget)
         widget.parent = self
         self._update_child_widgets()
         return widget
 
-    def add_grid(self, *args, **kwds):
+    def add_grid(self, *args, **kwargs):
         """
         Create a new Grid and add it as a child widget.
 
-        All arguments are given to add_widget().
+        All arguments are given to Grid().
         """
         from .grid import Grid
-        grid = Grid()
-        return self.add_widget(grid, *args, **kwds)
+        grid = Grid(*args, **kwargs)
+        return self.add_widget(grid)
 
-    def add_view(self, *args, **kwds):
+    def add_view(self, *args, **kwargs):
         """
         Create a new ViewBox and add it as a child widget.
 
-        All arguments are given to add_widget().
+        All arguments are given to ViewBox().
         """
         from .viewbox import ViewBox
-        view = ViewBox()
-        return self.add_widget(view, *args, **kwds)
+        view = ViewBox(*args, **kwargs)
+        return self.add_widget(view)
 
     def remove_widget(self, widget):
+        """
+        Remove a Widget as a managed child of this Widget.
+
+        Parameters
+        ----------
+        widget : instance of Widget
+            The widget to remove.
+        """
         self._widgets.remove(widget)
         widget.remove_parent(self)
         self._update_child_widgets()
diff --git a/vispy/testing/__init__.py b/vispy/testing/__init__.py
index 8fc460d..38e301d 100644
--- a/vispy/testing/__init__.py
+++ b/vispy/testing/__init__.py
@@ -1,8 +1,50 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+"""
+Testing
+=======
+This module provides functions useful for running tests in vispy.
+
+Tests can be run in a few ways:
+
+    * From Python, you can import ``vispy`` and do ``vispy.test()``.
+    * From the source root, you can do ``make test`` which wraps to
+      a call to ``python make test``.
+
+There are various diffrent testing "modes", including:
+
+    * "full": run all tests.
+    * any backend name (e.g., "glfw"): run application/GL tests using a
+      specific backend.
+    * "nobackend": run tests that do not require a backend.
+    * "examples": run repo examples to check for errors and warnings.
+    * "flake": check style errors.
+
+Examples get automatically tested unless they have a special comment toward
+the top ``# vispy: testskip``. Examples that should be tested should be
+formatted so that 1) a ``Canvas`` class is defined, or a ``canvas`` class
+is instantiated; and 2) the ``app.run()`` call is protected by a check
+if ``__name__ == "__main__"``. This makes it so that the event loop is not
+started when running examples in the test suite -- the test suite instead
+manually updates the canvas (using ``app.process_events()``) for under one
+second to ensure that things like timer events are processed.
+
+For examples on how to test various bits of functionality (e.g., application
+functionality, or drawing things with OpenGL), it's best to look at existing
+examples in the test suite.
+
+The code base gets automatically tested by Travis-CI (Linux) and AppVeyor
+(Windows) on Python 2.6, 2.7, 3.4. There are multiple testing modes that
+use e.g. full dependencies, minimal dependencies, etc. See ``.travis.yml``
+to determine what automatic tests are run.
+"""
 
 from ._testing import (SkipTest, requires_application, requires_img_lib,  # noqa
-                      assert_is, assert_in, assert_not_in, has_backend,  # noqa
-                      glut_skip, requires_pyopengl, requires_scipy,  # noqa
-                      has_matplotlib, assert_image_equal,  # noqa
-                      save_testing_image, TestingCanvas, has_pyopengl)  # noqa
-from ._runners import _tester  # noqa
+                      has_backend, requires_pyopengl,  # noqa
+                      requires_scipy, has_matplotlib,  # noqa
+                      save_testing_image, TestingCanvas, has_pyopengl,  # noqa
+                      run_tests_if_main,
+                      assert_is, assert_in, assert_not_in, assert_equal,
+                      assert_not_equal, assert_raises, assert_true)  # noqa
+from ._runners import test  # noqa
diff --git a/vispy/testing/_coverage.py b/vispy/testing/_coverage.py
deleted file mode 100644
index 3e1d20c..0000000
--- a/vispy/testing/_coverage.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-# Code inspired by original nose plugin:
-# https://nose.readthedocs.org/en/latest/plugins/cover.html
-
-from nose.plugins.base import Plugin
-
-
-class MutedCoverage(Plugin):
-    """Make a silent coverage report using Ned Batchelder's coverage module."""
-
-    def configure(self, options, conf):
-        Plugin.configure(self, options, conf)
-        self.enabled = True
-        try:
-            from coverage import coverage
-        except ImportError:
-            self.enabled = False
-            self.cov = None
-            print('Module "coverage" not installed, code coverage will not '
-                  'be available')
-        else:
-            self.enabled = True
-            self.cov = coverage(auto_data=False, branch=True, data_suffix=None,
-                                source=['vispy'])
-
-    def begin(self):
-        self.cov.load()
-        self.cov.start()
-
-    def report(self, stream):
-        self.cov.stop()
-        self.cov.combine()
-        self.cov.save()
diff --git a/vispy/testing/_runners.py b/vispy/testing/_runners.py
index 2827046..7d7fa69 100644
--- a/vispy/testing/_runners.py
+++ b/vispy/testing/_runners.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# vispy: testskip
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """Test running functions"""
 
@@ -8,13 +9,15 @@ from __future__ import print_function
 import sys
 import os
 from os import path as op
-from subprocess import Popen
 from copy import deepcopy
 from functools import partial
 
-from ..util import use_log_level
+from ..util import use_log_level, run_subprocess
 from ..util.ptime import time
-from ._testing import SkipTest, has_backend
+from ._testing import SkipTest, has_backend, has_application, nottest
+
+
+_line_sep = '-' * 70
 
 
 def _get_root_dir():
@@ -28,55 +31,67 @@ def _get_root_dir():
     return root_dir, dev
 
 
-def _nose(mode, verbosity, coverage, extra_args):
-    """Run nosetests using a particular mode"""
-    cwd = os.getcwd()  # this must be done before nose import
+_unit_script = """
+import pytest
+try:
+    import faulthandler
+    faulthandler.enable()
+except Exception:
+    pass
+
+raise SystemExit(pytest.main(%r))
+"""
+
+
+def _unit(mode, extra_arg_string):
+    """Run unit tests using a particular mode"""
+    cwd = os.getcwd()
     try:
-        import nose  # noqa, analysis:ignore
+        import pytest  # noqa, analysis:ignore
     except ImportError:
-        print('Skipping nosetests, nose not installed')
+        print('Skipping pytest, pytest not installed')
         raise SkipTest()
-    extra = ('-' * 70)
+
     if mode == 'nobackend':
-        print(extra + '\nRunning tests with no backend')
-        attrs = '-a !vispy_app_test '
-        app_import = ''
+        msg = 'Running tests with no backend'
+        extra_arg_string = '-m "not vispy_app_test" ' + extra_arg_string
+        coverage = True
     else:
         with use_log_level('warning', print_msg=False):
             has, why_not = has_backend(mode, out=['why_not'])
-        if has:
-            print('%s\nRunning tests with %s backend' % (extra, mode))
-            attrs = '-a vispy_app_test '
-        else:
+        if not has:
             msg = ('Skipping tests for backend %s, not found (%s)'
                    % (mode, why_not))
-            print(extra + '\n' + msg + '\n' + extra + '\n')  # last \n nicer
+            print(_line_sep + '\n' + msg + '\n' + _line_sep + '\n')
             raise SkipTest(msg)
-        app_import = '\nfrom vispy import use\nuse(app="%s")\n' % mode
-    sys.stdout.flush()
-    # we might as well always use coverage, since we manually disable printing!
-    # here we actually read in the Python code to avoid importing it from
-    # from vispy.testing._coverage, since doing so breaks some path stuff later
-    muted_file = op.join(op.dirname(__file__), '_coverage.py')
-    with open(muted_file, 'r') as fid:
-        imps = fid.read()
-    cv = ', addplugins=[MutedCoverage()]'
-    # if not coverage:
-    #    imps = ''
-    #    cv = ''
-    arg = (' ' + ('--verbosity=%s ' % verbosity) + attrs +
-           ' '.join(str(e) for e in extra_args))
+        msg = 'Running tests with %s backend' % mode
+        extra_arg_string = '-m vispy_app_test ' + extra_arg_string
+        coverage = True
+    if coverage:
+        extra_arg_string += ' --cov vispy --no-cov-on-fail '
     # make a call to "python" so that it inherits whatever the system
     # thinks is "python" (e.g., virtualenvs)
-    cmd = [sys.executable, '-c',
-           '%s%simport nose; nose.main(argv="%s".split(" ")%s)'
-           % (imps, app_import, arg, cv)]
+    cmd = [sys.executable, '-c', _unit_script % extra_arg_string]
     env = deepcopy(os.environ)
-    env.update(dict(_VISPY_TESTING_TYPE=mode))
-    p = Popen(cmd, cwd=cwd, env=env)
-    stdout, stderr = p.communicate()
-    if(p.returncode):
-        raise RuntimeError('Nose failure (%s):\n%s' % (p.returncode, stderr))
+
+    # We want to set this for all app backends plus "nobackend" to
+    # help ensure that app tests are appropriately decorated
+    env.update(dict(_VISPY_TESTING_APP=mode))
+    env_str = '_VISPY_TESTING_APP=%s ' % mode
+    if len(msg) > 0:
+        msg = ('%s\n%s:\n%s%s'
+               % (_line_sep, msg, env_str, extra_arg_string))
+        print(msg)
+    sys.stdout.flush()
+    return_code = run_subprocess(cmd, return_code=True, cwd=cwd, env=env,
+                                 stdout=None, stderr=None)[2]
+    if return_code:
+        raise RuntimeError('unit failure (%s)' % return_code)
+    else:
+        out_name = '.coverage.%s' % mode
+        if op.isfile(out_name):
+            os.remove(out_name)
+        os.rename('.coverage', out_name)
 
 
 def _flake():
@@ -88,10 +103,11 @@ def _flake():
         sys.argv[1:] = ['vispy', 'examples', 'make']
     else:
         sys.argv[1:] = ['vispy']
-    sys.argv.append('--ignore=E226,E241,E265,W291,W293')
+    sys.argv.append('--ignore=E226,E241,E265,E266,W291,W293,W503')
     sys.argv.append('--exclude=six.py,py24_ordereddict.py,glfw.py,'
-                    '_proxy.py,_angle.py,_desktop.py,_pyopengl.py,'
-                    '_constants.py,png.py')
+                    '_proxy.py,_es2.py,_gl2.py,_pyopengl2.py,'
+                    '_constants.py,png.py,decorator.py,ipy_inputhook.py,'
+                    'experimental,wiki,_old,mplexporter.py,cubehelix.py')
     try:
         from flake8.main import main
     except ImportError:
@@ -131,7 +147,8 @@ def _check_line_endings():
             relfilename = op.relpath(filename, root_dir)
             # Open and check
             try:
-                text = open(filename, 'rb').read().decode('utf-8')
+                with open(filename, 'rb') as fid:
+                    text = fid.read().decode('utf-8')
             except UnicodeDecodeError:
                 continue  # Probably a binary file
             crcount = text.count('\r')
@@ -146,32 +163,164 @@ def _check_line_endings():
                            % (len(report), '\n'.join(report)))
 
 
-def _tester(label='full', coverage=False, verbosity=1, extra_args=()):
-    """Test vispy software. See vispy.test()
+_script = """
+import sys
+import time
+import warnings
+try:
+    import faulthandler
+    faulthandler.enable()
+except Exception:
+    pass
+import {0}
+
+if hasattr({0}, 'canvas'):
+    canvas = {0}.canvas
+elif hasattr({0}, 'Canvas'):
+    canvas = {0}.Canvas()
+elif hasattr({0}, 'fig'):
+    canvas = {0}.fig
+else:
+    raise RuntimeError('Bad example formatting: fix or add `# vispy: testskip`'
+                       ' to the top of the file.')
+
+with canvas as c:
+    for _ in range(5):
+        c.update()
+        c.app.process_events()
+        time.sleep(1./60.)
+"""
+
+
+def _examples(fnames_str):
+    """Run examples and make sure they work.
+
+    Parameters
+    ----------
+    fnames_str : str
+        Can be a space-separated list of paths to test, or an empty string to
+        auto-detect and run all examples.
+    """
+    root_dir, dev = _get_root_dir()
+    reason = None
+    if not dev:
+        reason = 'Cannot test examples unless in vispy git directory'
+    else:
+        with use_log_level('warning', print_msg=False):
+            good, backend = has_application(capable=('multi_window',))
+        if not good:
+            reason = 'Must have suitable app backend'
+    if reason is not None:
+        msg = 'Skipping example test: %s' % reason
+        print(msg)
+        raise SkipTest(msg)
+
+    # if we're given individual file paths as a string in fnames_str,
+    # then just use them as the fnames
+    # otherwise, use the full example paths that have been
+    # passed to us
+    if fnames_str:
+        fnames = fnames_str.split(' ')
+
+    else:
+        fnames = [op.join(d[0], fname)
+                  for d in os.walk(op.join(root_dir, 'examples'))
+                  for fname in d[2] if fname.endswith('.py')]
+
+    fnames = sorted(fnames, key=lambda x: x.lower())
+    print(_line_sep + '\nRunning %s examples using %s backend'
+          % (len(fnames), backend))
+    op.join('tutorial', 'app', 'shared_context.py'),  # non-standard
+
+    fails = []
+    n_ran = n_skipped = 0
+    t0 = time()
+    for fname in fnames:
+        n_ran += 1
+        root_name = op.split(fname)
+        root_name = op.join(op.split(op.split(root_name[0])[0])[1],
+                            op.split(root_name[0])[1], root_name[1])
+        good = True
+        with open(fname, 'r') as fid:
+            for _ in range(10):  # just check the first 10 lines
+                line = fid.readline()
+                if line == '':
+                    break
+                elif line.startswith('# vispy: ') and 'testskip' in line:
+                    good = False
+                    break
+        if not good:
+            n_ran -= 1
+            n_skipped += 1
+            continue
+        sys.stdout.flush()
+        cwd = op.dirname(fname)
+        cmd = [sys.executable, '-c', _script.format(op.split(fname)[1][:-3])]
+        sys.stdout.flush()
+        stdout, stderr, retcode = run_subprocess(cmd, return_code=True,
+                                                 cwd=cwd, env=os.environ)
+        if retcode or len(stderr.strip()) > 0:
+            # Skipping due to missing dependency is okay
+            if "ImportError: " in stderr:
+                print('S', end='')
+            else:
+                ext = '\n' + _line_sep + '\n'
+                fails.append('%sExample %s failed (%s):%s%s%s'
+                             % (ext, root_name, retcode, ext, stderr, ext))
+                print(fails[-1])
+        else:
+            print('.', end='')
+        sys.stdout.flush()
+    print('')
+    t = (': %s failed, %s succeeded, %s skipped in %s seconds'
+         % (len(fails), n_ran - len(fails), n_skipped, round(time()-t0)))
+    if len(fails) > 0:
+        raise RuntimeError('Failed%s' % t)
+    print('Success%s' % t)
+
+
+ at nottest
+def test(label='full', extra_arg_string=''):
+    """Test vispy software
+
+    Parameters
+    ----------
+    label : str
+        Can be one of 'full', 'unit', 'nobackend', 'extra', 'lineendings',
+        'flake', or any backend name (e.g., 'qt').
+    extra_arg_string : str
+        Extra arguments to sent to ``pytest``.
     """
     from vispy.app.backends import BACKEND_NAMES as backend_names
     label = label.lower()
-    verbosity = int(verbosity)
-    cov = bool(coverage)
-    if cov and op.isfile('.coverage'):
-        os.remove('.coverage')
-    known_types = ['full', 'nose', 'lineendings', 'extra', 'flake',
-                   'nobackend'] + backend_names
-    if label not in known_types:
-        raise ValueError('label must be one of %s, or a backend name %s'
-                         % (known_types, backend_names))
+    label = 'pytest' if label == 'nose' else label
+    known_types = ['full', 'unit', 'lineendings', 'extra', 'flake',
+                   'nobackend', 'examples']
+
+    if label not in known_types + backend_names:
+        raise ValueError('label must be one of %s, or a backend name %s, '
+                         'not \'%s\'' % (known_types, backend_names, label))
     work_dir = _get_root_dir()[0]
     orig_dir = os.getcwd()
     # figure out what we actually need to run
     runs = []
-    if label in ('full', 'nose'):
+    if label in ('full', 'unit'):
         for backend in backend_names:
-            runs.append([partial(_nose, backend, verbosity, cov, extra_args),
+            runs.append([partial(_unit, backend, extra_arg_string),
                          backend])
     elif label in backend_names:
-        runs.append([partial(_nose, label, verbosity, cov, extra_args), label])
-    if label in ('full', 'nose', 'nobackend'):
-        runs.append([partial(_nose, 'nobackend', verbosity, cov, extra_args),
+        runs.append([partial(_unit, label, extra_arg_string), label])
+
+    if label == "examples":
+        # take the extra arguments so that specific examples can be run
+        runs.append([partial(_examples, extra_arg_string),
+                    'examples'])
+    elif label == 'full':
+        # run all the examples
+        runs.append([partial(_examples, ""), 'examples'])
+
+    if label in ('full', 'unit', 'nobackend'):
+        runs.append([partial(_unit, 'nobackend', extra_arg_string),
                      'nobackend'])
     if label in ('full', 'extra', 'lineendings'):
         runs.append([_check_line_endings, 'lineendings'])
@@ -192,10 +341,10 @@ def _tester(label='full', coverage=False, verbosity=1, extra_args=()):
         except Exception as exp:
             # this should only happen if we've screwed up the test setup
             fail += [run[1]]
-            print('Failed strangely: %s\n' % str(exp))
+            print('Failed strangely (%s): %s\n' % (type(exp), str(exp)))
             import traceback
-            type, value, tb = sys.exc_info()
-            traceback.print_exception(type, value, tb)
+            type_, value, tb = sys.exc_info()
+            traceback.print_exception(type_, value, tb)
         else:
             print('Passed\n')
         finally:
diff --git a/vispy/testing/_testing.py b/vispy/testing/_testing.py
index 77c5779..3e03dc1 100644
--- a/vispy/testing/_testing.py
+++ b/vispy/testing/_testing.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
@@ -9,30 +9,15 @@ from __future__ import print_function
 import numpy as np
 import sys
 import os
-import subprocess
 import inspect
-import base64
-try:
-    from nose.tools import nottest, assert_equal, assert_true
-except ImportError:
-    assert_equal = assert_true = None
-
-    class nottest(object):
-        def __init__(self, *args):
-            pass  # Avoid "object() takes no parameters"
 
 from distutils.version import LooseVersion
 
-from ..scene import SceneCanvas
-from ..ext.six.moves import http_client as httplib
-from ..ext.six.moves import urllib_parse as urllib
-from ..io import read_png, _make_png, _check_img_lib
+from ..ext.six import string_types
 from ..util import use_log_level
-from ..util.fetching import get_testing_file
-from .. import gloo
 
 ###############################################################################
-# Adapted from Python's unittest2 (which is wrapped by nose)
+# Adapted from Python's unittest2
 # http://docs.python.org/2/license.html
 
 try:
@@ -45,45 +30,6 @@ except ImportError:
             pass
 
 
-def run_subprocess(command):
-    """Run command using subprocess.Popen
-
-    Run command and wait for command to complete. If the return code was zero
-    then return, otherwise raise CalledProcessError.
-    By default, this will also add stdout= and stderr=subproces.PIPE
-    to the call to Popen to suppress printing to the terminal.
-
-    Parameters
-    ----------
-    command : list of str
-        Command to run as subprocess (see subprocess.Popen documentation).
-
-    Returns
-    -------
-    stdout : str
-        Stdout returned by the process.
-    stderr : str
-        Stderr returned by the process.
-    """
-    # code adapted with permission from mne-python
-    kwargs = dict(stderr=subprocess.PIPE, stdout=subprocess.PIPE)
-
-    p = subprocess.Popen(command, **kwargs)
-    stdout_, stderr = p.communicate()
-
-    output = (stdout_.decode('ascii'), stderr.decode('ascii'))
-    if p.returncode:
-        print(stdout_)
-        print(stderr)
-        err_fun = subprocess.CalledProcessError.__init__
-        if 'output' in inspect.getargspec(err_fun).args:
-            raise subprocess.CalledProcessError(p.returncode, command, output)
-        else:
-            raise subprocess.CalledProcessError(p.returncode, command)
-
-    return output
-
-
 def _safe_rep(obj, short=False):
     """Helper for assert_* ports"""
     try:
@@ -115,26 +61,65 @@ def _format_msg(msg, std_msg):
     return msg
 
 
+def nottest(func):
+    """Decorator to mark a function or method as *not* a test
+    """
+    func.__test__ = False
+    return func
+
+
+def assert_raises(exp, func, *args, **kwargs):
+    """Backport"""
+    try:
+        func(*args, **kwargs)
+    except exp:
+        return
+    std_msg = '%s not raised' % (_safe_rep(exp))
+    raise AssertionError(_format_msg(None, std_msg))
+
+
 def assert_in(member, container, msg=None):
-    """Backport for old nose.tools"""
+    """Backport"""
     if member in container:
         return
     std_msg = '%s not found in %s' % (_safe_rep(member), _safe_rep(container))
-    msg = _format_msg(msg, std_msg)
-    raise AssertionError(msg)
+    raise AssertionError(_format_msg(msg, std_msg))
+
+
+def assert_true(x, msg=None):
+    """Backport"""
+    if x:
+        return
+    std_msg = '%s is not True' % (_safe_rep(x),)
+    raise AssertionError(_format_msg(msg, std_msg))
+
+
+def assert_equal(x, y, msg=None):
+    """Backport"""
+    if x == y:
+        return
+    std_msg = '%s not equal to %s' % (_safe_rep(x), _safe_rep(y))
+    raise AssertionError(_format_msg(msg, std_msg))
+
+
+def assert_not_equal(x, y, msg=None):
+    """Backport"""
+    if x != y:
+        return
+    std_msg = '%s equal to %s' % (_safe_rep(x), _safe_rep(y))
+    raise AssertionError(_format_msg(msg, std_msg))
 
 
 def assert_not_in(member, container, msg=None):
-    """Backport for old nose.tools"""
+    """Backport"""
     if member not in container:
         return
     std_msg = '%s found in %s' % (_safe_rep(member), _safe_rep(container))
-    msg = _format_msg(msg, std_msg)
-    raise AssertionError(msg)
+    raise AssertionError(_format_msg(msg, std_msg))
 
 
 def assert_is(expr1, expr2, msg=None):
-    """Backport for old nose.tools"""
+    """Backport"""
     if expr1 is not expr2:
         std_msg = '%s is not %s' % (_safe_rep(expr1), _safe_rep(expr2))
         raise AssertionError(_format_msg(msg, std_msg))
@@ -161,13 +146,14 @@ def requires_pyopengl():
 
 def has_backend(backend, has=(), capable=(), out=()):
     from ..app.backends import BACKENDMAP
-    using = os.getenv('_VISPY_TESTING_BACKEND', None)
+    using = os.getenv('_VISPY_TESTING_APP', None)
     if using is not None and using != backend:
         # e.g., we are on  a 'pyglet' run but the test requires PyQt4
         ret = (False,) if len(out) > 0 else False
         for o in out:
             ret += (None,)
         return ret
+
     # let's follow the standard code path
     module_name = BACKENDMAP[backend.lower()][1]
     with use_log_level('warning', print_msg=False):
@@ -184,48 +170,52 @@ def has_backend(backend, has=(), capable=(), out=()):
     return ret
 
 
-def requires_application(backend=None, has=(), capable=()):
-    """Decorator for tests that require an application"""
+def has_application(backend=None, has=(), capable=()):
+    """Determine if a suitable app backend exists"""
     from ..app.backends import BACKEND_NAMES
     # avoid importing other backends if we don't need to
     if backend is None:
-        good = False
         for backend in BACKEND_NAMES:
             if has_backend(backend, has=has, capable=capable):
                 good = True
+                msg = backend
                 break
-        msg = 'Requires application backend'
+        else:
+            good = False
+            msg = 'Requires application backend'
     else:
         good, why = has_backend(backend, has=has, capable=capable,
                                 out=['why_not'])
-        msg = 'Requires %s: %s' % (backend, why)
+        if not good:
+            msg = 'Requires %s: %s' % (backend, why)
+        else:
+            msg = backend
+    return good, msg
 
-    # Actually construct the decorator
-    def skip_decorator(f):
-        import nose
-        f.vispy_app_test = True  # set attribute for easy run or not
 
-        def skipper(*args, **kwargs):
-            if not good:
-                raise SkipTest("Skipping test: %s: %s" % (f.__name__, msg))
-            else:
-                return f(*args, **kwargs)
-        return nose.tools.make_decorator(f)(skipper)
-    return skip_decorator
+def composed(*decs):
+    def deco(f):
+        for dec in reversed(decs):
+            f = dec(f)
+        return f
+    return deco
 
 
-def glut_skip():
-    """Helper to skip a test if GLUT is the current backend"""
-    # this is basically a knownfail tool for glut
-    from ..app import use_app
-    app = use_app()
-    if app.backend_name.lower() == 'glut':
-        raise SkipTest('GLUT unstable')
-    return  # otherwise it's fine
+def requires_application(backend=None, has=(), capable=()):
+    """Return a decorator for tests that require an application"""
+    good, msg = has_application(backend, has, capable)
+    dec_backend = np.testing.dec.skipif(not good, "Skipping test: %s" % msg)
+    try:
+        import pytest
+    except Exception:
+        return dec_backend
+    dec_app = pytest.mark.vispy_app_test
+    return composed(dec_app, dec_backend)
 
 
 def requires_img_lib():
     """Decorator for tests that require an image library"""
+    from ..io import _check_img_lib
     if sys.platform.startswith('win'):
         has_img_lib = False  # PIL breaks tests on windows (!)
     else:
@@ -252,7 +242,7 @@ def has_matplotlib(version='1.2'):
 
 def _has_scipy(min_version):
     try:
-        assert isinstance(min_version, str)
+        assert isinstance(min_version, string_types)
         import scipy  # noqa, analysis:ignore
         from distutils.version import LooseVersion
         this_version = LooseVersion(scipy.__version__)
@@ -269,98 +259,44 @@ def requires_scipy(min_version='0.13'):
                                  'Requires Scipy version >= %s' % min_version)
 
 
-def _save_failed_test(data, expect, filename):
-    commit, error = run_subprocess(['git', 'rev-parse',  'HEAD'])
-    name = filename.split('/')
-    name.insert(-1, commit.strip())
-    filename = '/'.join(name)
-    host = 'data.vispy.org'
-
-    # concatenate data, expect, and diff into a single image
-    ds = data.shape
-    es = expect.shape
-    if ds == es:
-        shape = (ds[0], ds[1] * 3 + 2, 4)
-        img = np.empty(shape, dtype=np.ubyte)
-        img[:] = 255
-        img[:, :ds[1], :ds[2]] = data
-        img[:, ds[1]+1:ds[1]*2+1, :ds[2]] = expect
-        img[:, ds[1]*2 + 2:, :ds[2]] = np.abs(data.astype(int) -
-                                              expect.astype(int))
-    else:
-        shape = (ds[0], ds[1] * 2 + 1, 4)
-        img = np.empty(shape, dtype=np.ubyte)
-        img[:] = 255
-        img[:ds[0], :ds[1], :ds[2]] = data
-        img[:es[0], ds[1]+1+es[1]:, :es[2]] = expect
-
-    png = _make_png(img)
-    conn = httplib.HTTPConnection(host)
-    req = urllib.urlencode({'name': filename,
-                            'data': base64.b64encode(png)})
-    conn.request('POST', '/upload.py', req)
-    response = conn.getresponse().read()
-    conn.close()
-    print("\nUpload to: \nhttp://%s/data/%s" % (host, filename))
-    if not response.startswith(b'OK'):
-        print("WARNING: Error uploading data to %s" % host)
-        print(response)
-
-
-def assert_image_equal(image, reference, limit=40):
-    """Downloads reference image and compares with image
-
-    Parameters
-    ----------
-    image: str, numpy.array
-        'screenshot' or image data
-    reference: str
-        'The filename on the remote ``test-data`` repository to download'
-    limit : int
-        Number of pixels that can differ in the image.
-    """
-    raise SkipTest("Image comparison disabled until polygon visual "
-                   "output is finalized.")
-    from ..gloo.util import _screenshot
-
-    if image == "screenshot":
-        image = _screenshot(alpha=False)
-    ref = read_png(get_testing_file(reference))[:, :, :3]
-
-    assert_equal(image.shape, ref.shape)
-
-    # check for minimum number of changed pixels, allowing for overall 1-pixel
-    # shift in any direcion
-    slices = [slice(0, -1), slice(0, None), slice(1, None)]
-    min_diff = np.inf
-    for i in range(3):
-        for j in range(3):
-            a = image[slices[i], slices[j]]
-            b = ref[slices[2-i], slices[2-j]]
-            diff = np.any(a != b, axis=2).sum()
-            if diff < min_diff:
-                min_diff = diff
-    try:
-        assert_true(min_diff <= limit,
-                    'min_diff (%s) > %s' % (min_diff, limit))
-    except AssertionError:
-        _save_failed_test(image, ref, reference)
-        raise
-
-
-class TestingCanvas(SceneCanvas):
-    def __init__(self, bgcolor='black', size=(100, 100)):
-        SceneCanvas.__init__(self, size=size, bgcolor=bgcolor)
-
-    def __enter__(self):
-        SceneCanvas.__enter__(self)
-        gloo.clear(color=self._bgcolor)
-        return self
-
-    def draw_visual(self, visual):
-        SceneCanvas.draw_visual(self, visual)
-        gloo.gl.glFlush()
-        gloo.gl.glFinish()
+ at nottest
+def TestingCanvas(bgcolor='black', size=(100, 100), dpi=None, **kwargs):
+    """Class wrapper to avoid importing scene until necessary"""
+    from ..scene import SceneCanvas
+
+    class TestingCanvas(SceneCanvas):
+        def __init__(self, bgcolor, size, dpi, **kwargs):
+            self._entered = False
+            kwargs['bgcolor'] = bgcolor
+            kwargs['size'] = size
+            kwargs['dpi'] = dpi
+            SceneCanvas.__init__(self, **kwargs)
+
+        def __enter__(self):
+            SceneCanvas.__enter__(self)
+            # sometimes our window can be larger than our requsted draw
+            # area (e.g. on Windows), and this messes up our tests that
+            # typically use very small windows. Here we "fix" it.
+            scale = np.array(self.physical_size) / np.array(self.size, float)
+            scale = int(np.round(np.mean(scale)))
+            self._wanted_vp = 0, 0, size[0] * scale, size[1] * scale
+            self.context.set_state(clear_color=self._bgcolor)
+            self.context.set_viewport(*self._wanted_vp)
+            self._entered = True
+            return self
+
+        def draw_visual(self, visual, event=None, viewport=None, clear=True):
+            if not self._entered:
+                return
+            if clear:
+                self.context.clear()
+            SceneCanvas.draw_visual(self, visual, event, viewport)
+            # must set this because draw_visual sets it back to the
+            # canvas size when it's done
+            self.context.set_viewport(*self._wanted_vp)
+            self.context.finish()
+
+    return TestingCanvas(bgcolor, size, dpi, **kwargs)
 
 
 @nottest
@@ -369,7 +305,61 @@ def save_testing_image(image, location):
     from ..util import make_png
     if image == "screenshot":
         image = _screenshot(alpha=False)
-    png = make_png(image)
-    f = open(location+'.png', 'wb')
-    f.write(png)
-    f.close()
+    with open(location+'.png', 'wb') as fid:
+        fid.write(make_png(image))
+
+
+ at nottest
+def run_tests_if_main():
+    """Run tests in a given file if it is run as a script"""
+    local_vars = inspect.currentframe().f_back.f_locals
+    if not local_vars.get('__name__', '') == '__main__':
+        return
+    # we are in a "__main__"
+    fname = local_vars['__file__']
+    # Run ourselves. post-mortem debugging!
+    try:
+        import faulthandler
+        faulthandler.enable()
+    except Exception:
+        pass
+    import __main__
+    try:
+        import pytest
+        pytest.main(['-s', '--tb=short', fname])
+    except ImportError:
+        print('==== Running tests in script\n==== %s' % fname)
+        run_tests_in_object(__main__)
+        print('==== Tests pass')
+
+
+def run_tests_in_object(ob):
+    # Setup
+    for name in dir(ob):
+        if name.lower().startswith('setup'):
+            print('Calling %s' % name)
+            getattr(ob, name)()
+    # Exec
+    for name in sorted(dir(ob), key=lambda x: x.lower()):  # consistent order
+        val = getattr(ob, name)
+        if name.startswith('_'):
+            continue
+        elif callable(val) and (name[:4] == 'test' or name[-4:] == 'test'):
+            print('Running test-func %s ... ' % name, end='')
+            try:
+                val()
+                print('ok')
+            except Exception as err:
+                if 'skiptest' in err.__class__.__name__.lower():
+                    print('skip')
+                else:
+                    raise
+        elif isinstance(val, type) and 'Test' in name:
+            print('== Running test-class %s' % name)
+            run_tests_in_object(val())
+            print('== Done with test-class %s' % name)
+    # Teardown
+    for name in dir(ob):
+        if name.lower().startswith('teardown'):
+            print('Calling %s' % name)
+            getattr(ob, name)()
diff --git a/vispy/testing/image_tester.py b/vispy/testing/image_tester.py
new file mode 100644
index 0000000..a9cea82
--- /dev/null
+++ b/vispy/testing/image_tester.py
@@ -0,0 +1,454 @@
+
+
+"""
+Procedure for unit-testing with images:
+
+1. Run unit tests at least once; this initializes a git clone of 
+   vispy/test-data in config['test_data_path']. This path is 
+   `~/.vispy/test-data` unless the config variable has been modified.
+
+2. Run individual test scripts with the --vispy-audit flag:
+
+       $ python vispy/visuals/tests/test_ellipse.py --vispy-audit
+
+   Any failing tests will
+   display the test results, standard image, and the differences between the
+   two. If the test result is bad, then press (f)ail. If the test result is 
+   good, then press (p)ass and the new image will be saved to the test-data
+   directory.
+
+3. After adding or changing test images, create a new commit:
+
+        $ cd ~/.vispy/test-data
+        $ git add ...
+        $ git commit -a
+        
+4. Look up the most recent tag name from the `test_data_tag` variable in
+   get_test_data_repo() below. Increment the tag name by 1 in the function
+   and create a new tag in the test-data repository:
+
+        $ git tag test-data-NNN
+        $ git push --tags origin master
+
+    This tag is used to ensure that each vispy commit is linked to a specific
+    commit in the test-data repository. This makes it possible to push new
+    commits to the test-data repository without interfering with existing
+    tests, and also allows unit tests to continue working on older vispy
+    versions.
+
+"""
+
+import time
+import os
+import sys
+import inspect
+import base64
+from subprocess import check_call, CalledProcessError
+import numpy as np
+
+from ..ext.six.moves import http_client as httplib
+from ..ext.six.moves import urllib_parse as urllib
+from .. import scene, config
+from ..io import read_png, write_png
+from ..gloo.util import _screenshot
+from ..util import run_subprocess
+
+
+tester = None
+
+
+def get_tester():
+    global tester
+    if tester is None:
+        tester = ImageTester()
+    return tester
+
+
+def assert_image_approved(image, standard_file, message=None, **kwargs):
+    """Check that an image test result matches a pre-approved standard.
+    
+    If the result does not match, then the user can optionally invoke a GUI
+    to compare the images and decide whether to fail the test or save the new
+    image as the standard. 
+    
+    This function will automatically clone the test-data repository into 
+    ~/.vispy/test-data. However, it is up to the user to ensure this repository
+    is kept up to date and to commit/push new images after they are saved.
+    
+    Parameters
+    ----------
+    image : (h, w, 4) ndarray or 'screenshot'
+        The test result to check
+    standard_file : str
+        The name of the approved test image to check against. This file name
+        is relative to the root of the vispy test-data repository and will
+        be automatically fetched.
+    message : str
+        A string description of the image. It is recommended to describe 
+        specific features that an auditor should look for when deciding whether
+        to fail a test.
+        
+    Extra keyword arguments are used to set the thresholds for automatic image
+    comparison (see ``assert_image_match()``).    
+    """
+    
+    if image == "screenshot":
+        image = _screenshot(alpha=True)
+    if message is None:
+        code = inspect.currentframe().f_back.f_code
+        message = "%s::%s" % (code.co_filename, code.co_name)
+        
+    # Make sure we have a test data repo available, possibly invoking git
+    data_path = get_test_data_repo()
+    
+    # Read the standard image if it exists
+    std_file = os.path.join(data_path, standard_file)
+    if not os.path.isfile(std_file):
+        std_image = None
+    else:
+        std_image = read_png(std_file)
+        
+    # If the test image does not match, then we go to audit if requested.
+    try:
+        if image.shape != std_image.shape:
+            # Allow im1 to be an integer multiple larger than im2 to account
+            # for high-resolution displays
+            ims1 = np.array(image.shape).astype(float)
+            ims2 = np.array(std_image.shape).astype(float)
+            sr = ims1 / ims2
+            if (sr[0] != sr[1] or not np.allclose(sr, np.round(sr)) or 
+               sr[0] < 1):
+                raise TypeError("Test result shape %s is not an integer factor"
+                                " larger than standard image shape %s." %
+                                (ims1, ims2))
+            sr = np.round(sr).astype(int)
+            image = downsample(image, sr[0], axis=(0, 1)).astype(image.dtype)
+        
+        assert_image_match(image, std_image, **kwargs)
+    except Exception:
+        if standard_file in git_status(data_path):
+            print("\n\nWARNING: unit test failed against modified standard "
+                  "image %s.\nTo revert this file, run `cd %s; git checkout "
+                  "%s`\n" % (std_file, data_path, standard_file))
+        if config['audit_tests']:
+            sys.excepthook(*sys.exc_info())
+            get_tester().test(image, std_image, message)
+            std_path = os.path.dirname(std_file)
+            print('Saving new standard image to "%s"' % std_file)
+            if not os.path.isdir(std_path):
+                os.makedirs(std_path)
+            write_png(std_file, image)
+        else:
+            if std_image is None:
+                raise Exception("Test standard %s does not exist." % std_file)
+            else:
+                if os.getenv('TRAVIS') is not None:
+                    _save_failed_test(image, std_image, standard_file)
+                raise
+
+
+def assert_image_match(im1, im2, min_corr=0.9, px_threshold=50., 
+                       px_count=None, max_px_diff=None, avg_px_diff=None, 
+                       img_diff=None):
+    """Check that two images match.
+    
+    Images that differ in shape or dtype will fail unconditionally.
+    Further tests for similarity depend on the arguments supplied.
+    
+    Parameters
+    ----------
+    im1 : (h, w, 4) ndarray
+        Test output image
+    im2 : (h, w, 4) ndarray
+        Test standard image
+    min_corr : float or None
+        Minimum allowed correlation coefficient between corresponding image
+        values (see numpy.corrcoef)
+    px_threshold : float
+        Minimum value difference at which two pixels are considered different
+    px_count : int or None
+        Maximum number of pixels that may differ
+    max_px_diff : float or None
+        Maximum allowed difference between pixels
+    avg_px_diff : float or None
+        Average allowed difference between pixels
+    img_diff : float or None
+        Maximum allowed summed difference between images 
+        
+    """
+    assert im1.ndim == 3
+    assert im1.shape[2] == 4
+    assert im1.dtype == im2.dtype
+    
+    diff = im1.astype(float) - im2.astype(float)
+    if img_diff is not None:
+        assert np.abs(diff).sum() <= img_diff
+        
+    pxdiff = diff.max(axis=2)  # largest value difference per pixel
+    mask = np.abs(pxdiff) >= px_threshold
+    if px_count is not None:
+        assert mask.sum() <= px_count
+        
+    masked_diff = diff[mask]
+    if max_px_diff is not None and masked_diff.size > 0:
+        assert masked_diff.max() <= max_px_diff
+    if avg_px_diff is not None and masked_diff.size > 0:
+        assert masked_diff.mean() <= avg_px_diff
+
+    if min_corr is not None:
+        with np.errstate(invalid='ignore'):
+            corr = np.corrcoef(im1.ravel(), im2.ravel())[0, 1]
+        assert corr >= min_corr
+
+
+def _save_failed_test(data, expect, filename):
+    from ..io import _make_png
+    commit, error = run_subprocess(['git', 'rev-parse',  'HEAD'])
+    name = filename.split('/')
+    name.insert(-1, commit.strip())
+    filename = '/'.join(name)
+    host = 'data.vispy.org'
+
+    # concatenate data, expect, and diff into a single image
+    ds = data.shape
+    es = expect.shape
+    
+    shape = (max(ds[0], es[0]) + 4, ds[1] + es[1] + 8 + max(ds[1], es[1]), 4)
+    img = np.empty(shape, dtype=np.ubyte)
+    img[..., :3] = 100
+    img[..., 3] = 255
+    
+    img[2:2+ds[0], 2:2+ds[1], :ds[2]] = data
+    img[2:2+es[0], ds[1]+4:ds[1]+4+es[1], :es[2]] = expect
+    
+    diff = make_diff_image(data, expect)
+    img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff
+
+    png = _make_png(img)
+    conn = httplib.HTTPConnection(host)
+    req = urllib.urlencode({'name': filename,
+                            'data': base64.b64encode(png)})
+    conn.request('POST', '/upload.py', req)
+    response = conn.getresponse().read()
+    conn.close()
+    print("\nImage comparison failed. Test result: %s %s   Expected result: "
+          "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype))
+    print("Uploaded to: \nhttp://%s/data/%s" % (host, filename))
+    if not response.startswith(b'OK'):
+        print("WARNING: Error uploading data to %s" % host)
+        print(response)
+
+
+def make_diff_image(im1, im2):
+    """Return image array showing the differences between im1 and im2.
+    
+    Handles images of different shape. Alpha channels are not compared.
+    """
+    ds = im1.shape
+    es = im2.shape
+    
+    diff = np.empty((max(ds[0], es[0]), max(ds[1], es[1]), 4), dtype=int)
+    diff[..., :3] = 128
+    diff[..., 3] = 255
+    diff[:ds[0], :ds[1], :min(ds[2], 3)] += im1[..., :3]
+    diff[:es[0], :es[1], :min(es[2], 3)] -= im2[..., :3]
+    diff = np.clip(diff, 0, 255).astype(np.ubyte)
+    return diff
+
+
+def downsample(data, n, axis=0):
+    """Downsample by averaging points together across axis.
+    If multiple axes are specified, runs once per axis.
+    """
+    if hasattr(axis, '__len__'):
+        if not hasattr(n, '__len__'):
+            n = [n]*len(axis)
+        for i in range(len(axis)):
+            data = downsample(data, n[i], axis[i])
+        return data
+    
+    if n <= 1:
+        return data
+    nPts = int(data.shape[axis] / n)
+    s = list(data.shape)
+    s[axis] = nPts
+    s.insert(axis+1, n)
+    sl = [slice(None)] * data.ndim
+    sl[axis] = slice(0, nPts*n)
+    d1 = data[tuple(sl)]
+    d1.shape = tuple(s)
+    d2 = d1.mean(axis+1)
+    
+    return d2
+
+
+class ImageTester(scene.SceneCanvas):
+    """Graphical interface for auditing image comparison tests.
+    """
+    def __init__(self):
+        scene.SceneCanvas.__init__(self, size=(1000, 800))
+        self.bgcolor = (0.1, 0.1, 0.1, 1)
+        self.grid = self.central_widget.add_grid()
+        border = (0.3, 0.3, 0.3, 1)
+        self.views = (self.grid.add_view(row=0, col=0, border_color=border), 
+                      self.grid.add_view(row=0, col=1, border_color=border),
+                      self.grid.add_view(row=0, col=2, border_color=border))
+        label_text = ['test output', 'standard', 'diff']
+        for i, v in enumerate(self.views):
+            v.camera = 'panzoom'
+            v.camera.aspect = 1
+            v.camera.flip = (False, True)
+            v.image = scene.Image(parent=v.scene)
+            v.label = scene.Text(label_text[i], parent=v, color='yellow', 
+                                 anchor_x='left', anchor_y='top')
+        
+        self.views[1].camera.link(self.views[0].camera)
+        self.views[2].camera.link(self.views[0].camera)
+        self.console = scene.Console(text_color='white', border_color=border)
+        self.grid.add_widget(self.console, row=1, col=0, col_span=3)
+        
+    def test(self, im1, im2, message):
+        self.show()
+        self.console.write('------------------')
+        self.console.write(message)
+        if im2 is None:
+            self.console.write('Image1: %s %s   Image2: [no standard]' % 
+                               (im1.shape, im1.dtype))
+            im2 = np.zeros((1, 1, 3), dtype=np.ubyte)
+        else:
+            self.console.write('Image1: %s %s   Image2: %s %s' % 
+                               (im1.shape, im1.dtype, im2.shape, im2.dtype))
+        self.console.write('(P)ass or (F)ail this test?')
+        self.views[0].image.set_data(im1)
+        self.views[1].image.set_data(im2)
+        diff = make_diff_image(im1, im2)
+
+        self.views[2].image.set_data(diff)
+        self.views[0].camera.set_range()
+        
+        self.last_key = None
+        while True:
+            self.app.process_events()
+            if self.last_key is None:
+                pass
+            elif self.last_key.lower() == 'p':
+                self.console.write('PASS')
+                break
+            elif self.last_key.lower() in ('f', 'esc'):
+                self.console.write('FAIL')
+                raise Exception("User rejected test result.")
+            time.sleep(0.03)
+        
+        for v in self.views:
+            v.image.set_data(np.zeros((1, 1, 3), dtype=np.ubyte))
+
+    def on_key_press(self, event):
+        self.last_key = event.key.name
+
+
+def get_test_data_repo():
+    """Return the path to a git repository with the required commit checked
+    out. 
+    
+    If the repository does not exist, then it is cloned from
+    https://github.com/vispy/test-data. If the repository already exists
+    then the required commit is checked out.
+    """
+    
+    # This tag marks the test-data commit that this version of vispy should 
+    # be tested against. When adding or changing test images, create
+    # and push a new tag and update this variable.
+    test_data_tag = 'test-data-1'
+    
+    data_path = config['test_data_path']
+    git_path = 'https://github.com/vispy/test-data'
+    gitbase = git_cmd_base(data_path)
+    
+    if os.path.isdir(data_path):
+        # Already have a test-data repository to work with.
+        
+        # Get the commit ID of test_data_tag. Do a fetch if necessary.
+        try:
+            tag_commit = git_commit_id(data_path, test_data_tag)
+        except NameError:
+            cmd = gitbase + ['fetch', '--tags', 'origin']
+            print(' '.join(cmd))
+            check_call(cmd)
+            try:
+                tag_commit = git_commit_id(data_path, test_data_tag)
+            except NameError:
+                raise Exception("Could not find tag '%s' in test-data repo at"
+                                " %s" % (test_data_tag, data_path))
+        except Exception:
+            if not os.path.exists(os.path.join(data_path, '.git')):
+                raise Exception("Directory '%s' does not appear to be a git "
+                                "repository. Please remove this directory." % 
+                                data_path)
+            else:
+                raise
+            
+        # If HEAD is not the correct commit, then do a checkout
+        if git_commit_id(data_path, 'HEAD') != tag_commit:
+            print("Checking out test-data tag '%s'" % test_data_tag)
+            check_call(gitbase + ['checkout', test_data_tag])
+            
+    else:
+        print("Attempting to create git clone of test data repo in %s.." %
+              data_path)
+        
+        parent_path = os.path.split(data_path)[0]
+        if not os.path.isdir(parent_path):
+            os.makedirs(parent_path)
+        
+        if os.getenv('TRAVIS') is not None:
+            # Create a shallow clone of the test-data repository (to avoid
+            # downloading more data than is necessary)
+            os.makedirs(data_path)
+            cmds = [
+                gitbase + ['init'],
+                gitbase + ['remote', 'add', 'origin', git_path],
+                gitbase + ['fetch', '--tags', 'origin', test_data_tag,
+                           '--depth=1'],
+                gitbase + ['checkout', '-b', 'master', 'FETCH_HEAD'],
+            ]
+        else:
+            # Create a full clone
+            cmds = [['git', 'clone', git_path, data_path]]
+        
+        for cmd in cmds:
+            print(' '.join(cmd))
+            rval = check_call(cmd)
+            if rval == 0:
+                continue
+            raise RuntimeError("Test data path '%s' does not exist and could "
+                               "not be created with git. Either create a git "
+                               "clone of %s or set the test_data_path "
+                               "variable to an existing clone." % 
+                               (data_path, git_path))
+    
+    return data_path
+
+
+def git_cmd_base(path):
+    return ['git', '--git-dir=%s/.git' % path, '--work-tree=%s' % path]
+
+
+def git_status(path):
+    """Return a string listing all changes to the working tree in a git
+    repository.
+    """
+    cmd = git_cmd_base(path) + ['status', '--porcelain']
+    return run_subprocess(cmd, stderr=None, universal_newlines=True)[0]
+
+
+def git_commit_id(path, ref):
+    """Return the commit id of *ref* in the git repository at *path*.
+    """
+    cmd = git_cmd_base(path) + ['show', ref]
+    try:
+        output = run_subprocess(cmd, stderr=None, universal_newlines=True)[0]
+    except CalledProcessError:
+        raise NameError("Unknown git reference '%s'" % ref)
+    commit = output.split('\n')[0]
+    assert commit[:7] == 'commit '
+    return commit[7:]
diff --git a/vispy/testing/tests/test_testing.py b/vispy/testing/tests/test_testing.py
index a497bbc..802d22e 100644
--- a/vispy/testing/tests/test_testing.py
+++ b/vispy/testing/tests/test_testing.py
@@ -1,5 +1,10 @@
-from nose.tools import assert_raises
-from vispy.testing import assert_in, assert_not_in, assert_is
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+from vispy.testing import (assert_in, assert_not_in, assert_is,
+                           run_tests_if_main, assert_raises)
 
 
 def test_testing():
@@ -10,3 +15,6 @@ def test_testing():
     assert_not_in('foo', 'bar')
     assert_raises(AssertionError, assert_is, None, 0)
     assert_is(None, None)
+
+
+run_tests_if_main()
diff --git a/vispy/util/__init__.py b/vispy/util/__init__.py
index 05eb96f..f7fb723 100644
--- a/vispy/util/__init__.py
+++ b/vispy/util/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ Utilities for Vispy. A collection of modules that are used in
@@ -7,10 +7,10 @@ one or more Vispy sub-packages.
 """
 
 from .logs import logger, set_log_level, use_log_level  # noqa
-from .config import (_parse_command_line_arguments, config, sys_info,  # noqa
-                     save_config, get_config_keys, set_data_dir,  # noqa
-                     _TempDir)  # noqa
-
+from .config import (config, sys_info, save_config, get_config_keys,  # noqa 
+                     set_data_dir, _TempDir)  # noqa
+from .fetching import load_data_file  # noqa
 from . import fonts       # noqa
 from . import transforms  # noqa
-from .wrappers import test, use, run_subprocess  # noqa
+from .wrappers import use, run_subprocess  # noqa
+from .bunch import SimpleBunch  # noqa
diff --git a/vispy/util/bunch.py b/vispy/util/bunch.py
new file mode 100644
index 0000000..8a889ef
--- /dev/null
+++ b/vispy/util/bunch.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+
+# Class adapted from mne-python
+
+class SimpleBunch(dict):
+    """ Container object for datasets: dictionnary-like object that
+        exposes its keys as attributes.
+    """
+
+    def __init__(self, **kwargs):
+        dict.__init__(self, kwargs)
+        self.__dict__ = self
diff --git a/vispy/util/config.py b/vispy/util/config.py
index 70c7124..ad33550 100644
--- a/vispy/util/config.py
+++ b/vispy/util/config.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """Vispy configuration functions
@@ -17,29 +17,160 @@ import atexit
 from shutil import rmtree
 
 from .event import EmitterGroup, EventEmitter, Event
-from ..ext.six import string_types
 from .logs import logger, set_log_level, use_log_level
+from ..ext.six import string_types, file_types
 
+config = None
+_data_path = None
+_allowed_config_keys = None
 
-class _TempDir(str):
-    """Class for creating and auto-destroying temp dir
-
-    This is designed to be used with testing modules.
 
-    We cannot simply use __del__() method for cleanup here because the rmtree
-    function may be cleaned up before this object, so we use the atexit module
-    instead.
+def _init():
+    """ Create global Config object, parse command flags
     """
-    def __new__(self):
-        new = str.__new__(self, tempfile.mkdtemp())
-        return new
+    global config, _data_path, _allowed_config_keys
+
+    app_dir = _get_vispy_app_dir()
+    if app_dir is not None:
+        _data_path = op.join(app_dir, 'data')
+        _test_data_path = op.join(app_dir, 'test_data')
+    else:
+        _data_path = _test_data_path = None
+
+    # All allowed config keys and the types they may have
+    _allowed_config_keys = {
+        'data_path': string_types,
+        'default_backend': string_types,
+        'gl_backend': string_types,
+        'gl_debug': (bool,),
+        'glir_file': string_types+file_types,
+        'include_path': list,
+        'logging_level': string_types,
+        'qt_lib': string_types,
+        'dpi': (int, type(None)),
+        'profile': string_types + (type(None),),
+        'audit_tests': (bool,),
+        'test_data_path': string_types + (type(None),),
+    }
+
+    # Default values for all config options
+    default_config_options = {
+        'data_path': _data_path,
+        'default_backend': '',
+        'gl_backend': 'gl2',
+        'gl_debug': False,
+        'glir_file': '',
+        'include_path': [],
+        'logging_level': 'info',
+        'qt_lib': 'any',
+        'dpi': None,
+        'profile': None,
+        'audit_tests': False,
+        'test_data_path': _test_data_path,
+    }
+
+    config = Config(**default_config_options)
 
-    def __init__(self):
-        self._path = self.__str__()
-        atexit.register(self.cleanup)
+    try:
+        config.update(**_load_config())
+    except Exception as err:
+        raise Exception('Error while reading vispy config file "%s":\n  %s' %
+                        (_get_config_fname(), err.message))
+    set_log_level(config['logging_level'])
 
-    def cleanup(self):
-        rmtree(self._path, ignore_errors=True)
+    _parse_command_line_arguments()
+
+
+###############################################################################
+# Command line flag parsing
+
+VISPY_HELP = """
+VisPy command line arguments:
+
+  --vispy-backend=(qt|pyqt4|pyt5|pyside|glfw|pyglet|sdl2|wx)
+    Selects the backend system for VisPy to use. This will override the default
+    backend selection in your configuration file.
+
+  --vispy-log=(debug|info|warning|error|critical)[,search string]
+    Sets the verbosity of logging output. The default is 'warning'. If a search
+    string is given, messages will only be displayed if they match the string,
+    or if their call location (module.class:method(line) or
+    module:function(line)) matches the string.
+
+  --vispy-dpi=resolution
+    Force the screen resolution to a certain value (in pixels per inch). By
+    default, the OS is queried to determine the screen DPI.
+
+  --vispy-fps
+    Print the framerate (in Frames Per Second) in the console.
+
+  --vispy-gl-debug
+    Enables error checking for all OpenGL calls.
+
+  --vispy-glir-file
+    Export glir commands to specified file.
+
+  --vispy-profile=locations
+    Measure performance at specific code locations and display results. 
+    *locations* may be "all" or a comma-separated list of method names like
+    "SceneCanvas.draw_visual".
+
+  --vispy-cprofile
+    Enable profiling using the built-in cProfile module and display results
+    when the program exits.
+
+  --vispy-audit-tests
+    Enable user auditing of image test results.
+
+  --vispy-help
+    Display this help message.
+
+"""
+
+
+def _parse_command_line_arguments():
+    """ Transform vispy specific command line args to vispy config.
+    Put into a function so that any variables dont leak in the vispy namespace.
+    """
+    global config
+    # Get command line args for vispy
+    argnames = ['vispy-backend=', 'vispy-gl-debug', 'vispy-glir-file=',
+                'vispy-log=', 'vispy-help', 'vispy-profile=', 'vispy-cprofile',
+                'vispy-dpi=', 'vispy-audit-tests']
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], '', argnames)
+    except getopt.GetoptError:
+        opts = []
+    # Use them to set the config values
+    for o, a in opts:
+        if o.startswith('--vispy'):
+            if o == '--vispy-backend':
+                config['default_backend'] = a
+                logger.info('vispy backend: %s', a)
+            elif o == '--vispy-gl-debug':
+                config['gl_debug'] = True
+            elif o == '--vispy-glir-file':
+                config['glir_file'] = a
+            elif o == '--vispy-log':
+                if ',' in a:
+                    verbose, match = a.split(',')
+                else:
+                    verbose = a
+                    match = None
+                config['logging_level'] = a
+                set_log_level(verbose, match)
+            elif o == '--vispy-profile':
+                config['profile'] = a
+            elif o == '--vispy-cprofile':
+                _enable_profiling()
+            elif o == '--vispy-help':
+                print(VISPY_HELP)
+            elif o == '--vispy-dpi':
+                config['dpi'] = int(a)
+            elif o == '--vispy-audit-tests':
+                config['audit_tests'] = True
+            else:
+                logger.warning("Unsupported vispy flag: %s" % o)
 
 
 ###############################################################################
@@ -108,7 +239,6 @@ class Config(object):
     Config.events.changed - Emits ConfigEvent whenever the configuration
     changes.
     """
-
     def __init__(self, **kwargs):
         self.events = EmitterGroup(source=self)
         self.events['changed'] = EventEmitter(
@@ -128,14 +258,15 @@ class Config(object):
         self.events.changed(changes={item: val})
 
     def _check_key_val(self, key, val):
+        global _allowed_config_keys
         # check values against acceptable ones
-        known_keys = get_config_keys()
+        known_keys = _allowed_config_keys
         if key not in known_keys:
             raise KeyError('key "%s" not in known keys: "%s"'
                            % (key, known_keys))
-        if not isinstance(val, (string_types, bool)):
-            raise TypeError('Value for key "%s" must be str or bool, not %s'
-                            % (key, type(val)))
+        if not isinstance(val, known_keys[key]):
+            raise TypeError('Value for key "%s" must be one of %s, not %s.'
+                            % (key, known_keys[key], type(val)))
 
     def update(self, **kwargs):
         for key, val in kwargs.items():
@@ -148,15 +279,15 @@ class Config(object):
 
 
 def get_config_keys():
-    """The config keys known by vispy
+    """The config keys known by vispy and their allowed data types.
 
     Returns
     -------
-    keys : tuple
-        List of known config keys.
+    keys : dict
+        Dict of {key: (types,)} pairs.
     """
-    return ('data_path', 'default_backend', 'gl_debug', 'logging_level',
-            'qt_lib')
+    global _allowed_config_keys
+    return _allowed_config_keys.copy()
 
 
 def _get_config_fname():
@@ -202,22 +333,18 @@ def save_config(**kwargs):
         json.dump(current_config, fid, sort_keys=True, indent=0)
 
 
-_data_path = _get_vispy_app_dir()
-if _data_path is not None:
-    _data_path = op.join(_data_path, 'data')
-config = Config(default_backend='qt', qt_lib='any',
-                gl_debug=False, logging_level='info',
-                data_path=_data_path)
-try:
-    config.update(**_load_config())
-except Exception as err:
-    raise Exception('Error while reading vispy config file "%s":\n  %s' %
-                    (_get_config_fname(), err.message))
-set_log_level(config['logging_level'])
-
-
 def set_data_dir(directory=None, create=False, save=False):
-    """Set vispy data download directory"""
+    """Set vispy data download directory
+
+    Parameters
+    ----------
+    directory : str | None
+        The directory to use.
+    create : bool
+        If True, create directory if it doesn't exist.
+    save : bool
+        If True, save the configuration to the vispy config.
+    """
     if directory is None:
         directory = _data_path
         if _data_path is None:
@@ -233,72 +360,6 @@ def set_data_dir(directory=None, create=False, save=False):
         save_config(data_path=directory)
 
 
-###############################################################################
-# System information and parsing
-
-VISPY_HELP = """
-VisPy command line arguments:
-
-  --vispy-backend=(qt|pyqt|pyside|glut|glfw|pyglet)
-    Selects the backend system for VisPy to use. This will override the default
-    backend selection in your configuration file.
-    
-  --vispy-log=(debug|info|warning|error|critical)[,search string]
-    Sets the verbosity of logging output. The default is 'warning'. If a search
-    string is given, messages will only be displayed if they match the string,
-    or if their call location (module.class:method(line) or 
-    module:function(line)) matches the string.    
-    
-  --vispy-fps
-    Print the framerate (in Frames Per Second) in the console.
-    
-  --vispy-gl-debug
-    Enables error checking for all OpenGL calls.
-
-  --vispy-profile
-    Enable profiling and print the results when the program exits.
-    
-  --vispy-help
-    Display this help message.
-
-"""
-
-
-def _parse_command_line_arguments():
-    """ Transform vispy specific command line args to vispy config.
-    Put into a function so that any variables dont leak in the vispy namespace.
-    """
-    # Get command line args for vispy
-    argnames = ['vispy-backend=', 'vispy-gl-debug', 'vispy-log=', 'vispy-help',
-                'vispy-profile']
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], '', argnames)
-    except getopt.GetoptError:
-        opts = []
-    # Use them to set the config values
-    for o, a in opts:
-        if o.startswith('--vispy'):
-            if o == '--vispy-backend':
-                config['default_backend'] = a
-                logger.info('vispy backend: %s', a)
-            elif o == '--vispy-gl-debug':
-                config['gl_debug'] = True
-            elif o == '--vispy-log':
-                if ',' in a:
-                    verbose, match = a.split(',')
-                else:
-                    verbose = a
-                    match = None
-                config['logging_level'] = a
-                set_log_level(verbose, match)
-            elif o == '--vispy-profile':
-                _enable_profiling()
-            elif o == '--vispy-help':
-                print(VISPY_HELP)
-            else:
-                logger.warning("Unsupported vispy flag: %s" % o)
-
-
 def _enable_profiling():
     """ Start profiling and register callback to print stats when the program
     exits.
@@ -346,27 +407,25 @@ def sys_info(fname=None, overwrite=False):
         from ..testing import has_backend
         # get default app
         with use_log_level('warning'):
-            app = use_app()  # suppress messages
+            app = use_app(call_reuse=False)  # suppress messages
         out += 'Platform: %s\n' % platform.platform()
         out += 'Python:   %s\n' % str(sys.version).replace('\n', ' ')
         out += 'Backend:  %s\n' % app.backend_name
         for backend in BACKEND_NAMES:
+            if backend.startswith('ipynb_'):
+                continue
             with use_log_level('warning', print_msg=False):
                 which = has_backend(backend, out=['which'])[1]
             out += '{0:<9} {1}\n'.format(backend + ':', which)
         out += '\n'
         # We need an OpenGL context to get GL info
-        if 'glut' in app.backend_name.lower():
-            # glut causes problems
-            out += 'OpenGL information omitted for glut backend\n'
-        else:
-            canvas = Canvas('Test', (10, 10), show=False, app=app)
-            canvas._backend._vispy_set_current()
-            out += 'GL version:  %s\n' % gl.glGetParameter(gl.GL_VERSION)
-            x_ = gl.GL_MAX_TEXTURE_SIZE
-            out += 'MAX_TEXTURE_SIZE: %d\n' % gl.glGetParameter(x_)
-            out += 'Extensions: %s\n' % gl.glGetParameter(gl.GL_EXTENSIONS)
-            canvas.close()
+        canvas = Canvas('Test', (10, 10), show=False, app=app)
+        canvas._backend._vispy_set_current()
+        out += 'GL version:  %r\n' % (gl.glGetParameter(gl.GL_VERSION),)
+        x_ = gl.GL_MAX_TEXTURE_SIZE
+        out += 'MAX_TEXTURE_SIZE: %r\n' % (gl.glGetParameter(x_),)
+        out += 'Extensions: %r\n' % (gl.glGetParameter(gl.GL_EXTENSIONS),)
+        canvas.close()
     except Exception:  # don't stop printing info
         out += '\nInfo-gathering error:\n%s' % traceback.format_exc()
         pass
@@ -374,3 +433,28 @@ def sys_info(fname=None, overwrite=False):
         with open(fname, 'w') as fid:
             fid.write(out)
     return out
+
+
+class _TempDir(str):
+    """Class for creating and auto-destroying temp dir
+
+    This is designed to be used with testing modules.
+
+    We cannot simply use __del__() method for cleanup here because the rmtree
+    function may be cleaned up before this object, so we use the atexit module
+    instead.
+    """
+    def __new__(self):
+        new = str.__new__(self, tempfile.mkdtemp())
+        return new
+
+    def __init__(self):
+        self._path = self.__str__()
+        atexit.register(self.cleanup)
+
+    def cleanup(self):
+        rmtree(self._path, ignore_errors=True)
+
+
+# initialize config options
+_init()
diff --git a/vispy/util/dpi/__init__.py b/vispy/util/dpi/__init__.py
new file mode 100644
index 0000000..bf5779a
--- /dev/null
+++ b/vispy/util/dpi/__init__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+The dpi module enables querying the OS for the screen DPI.
+"""
+
+import sys
+
+__all__ = ['get_dpi']
+
+if sys.platform.startswith('linux'):
+    from ._linux import get_dpi
+elif sys.platform == 'darwin':
+    from ._quartz import get_dpi
+elif sys.platform.startswith('win'):
+    from ._win32 import get_dpi  # noqa, analysis:ignore
+else:
+    raise NotImplementedError('unknown system %s' % sys.platform)
diff --git a/vispy/util/dpi/_linux.py b/vispy/util/dpi/_linux.py
new file mode 100644
index 0000000..27be869
--- /dev/null
+++ b/vispy/util/dpi/_linux.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+
+import re
+from subprocess import CalledProcessError
+
+from ..logs import logger
+from ..wrappers import run_subprocess
+
+
+def _get_dpi_from(cmd, pattern, func):
+    """Match pattern against the output of func, passing the results as
+    floats to func.  If anything fails, return None.
+    """
+    try:
+        out, _ = run_subprocess([cmd])
+    except (OSError, CalledProcessError):
+        pass
+    else:
+        match = re.search(pattern, out)
+        if match:
+            return func(*map(float, match.groups()))
+
+
+def get_dpi(raise_error=True):
+    """Get screen DPI from the OS
+
+    Parameters
+    ----------
+    raise_error : bool
+        If True, raise an error if DPI could not be determined.
+
+    Returns
+    -------
+    dpi : float
+        Dots per inch of the primary screen.
+    """
+
+    from_xdpyinfo = _get_dpi_from(
+        'xdpyinfo', r'(\d+)x(\d+) dots per inch',
+        lambda x_dpi, y_dpi: (x_dpi + y_dpi) / 2)
+    if from_xdpyinfo is not None:
+        return from_xdpyinfo
+
+    from_xrandr = _get_dpi_from(
+        'xrandr', r'(\d+)x(\d+).*?(\d+)mm x (\d+)mm',
+        lambda x_px, y_px, x_mm, y_mm: 25.4 * (x_px / x_mm + y_px / y_mm) / 2)
+    if from_xrandr is not None:
+        return from_xrandr
+    if raise_error:
+        raise RuntimeError('could not determine DPI')
+    else:
+        logger.warning('could not determine DPI')
+    return 96
diff --git a/vispy/util/dpi/_quartz.py b/vispy/util/dpi/_quartz.py
new file mode 100644
index 0000000..3605d98
--- /dev/null
+++ b/vispy/util/dpi/_quartz.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+from ...ext.cocoapy import quartz
+
+
+def get_dpi(raise_error=True):
+    """Get screen DPI from the OS
+
+    Parameters
+    ----------
+    raise_error : bool
+        If True, raise an error if DPI could not be determined.
+
+    Returns
+    -------
+    dpi : float
+        Dots per inch of the primary screen.
+    """
+    display = quartz.CGMainDisplayID()
+    mm = quartz.CGDisplayScreenSize(display)
+    px = quartz.CGDisplayBounds(display).size
+    return (px.width/mm.width + px.height/mm.height) * 0.5 * 25.4
diff --git a/vispy/util/dpi/_win32.py b/vispy/util/dpi/_win32.py
new file mode 100644
index 0000000..36c2db9
--- /dev/null
+++ b/vispy/util/dpi/_win32.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+from ...ext.gdi32plus import (gdi32, user32, HORZSIZE, VERTSIZE,
+                              HORZRES, VERTRES)
+
+
+def get_dpi(raise_error=True):
+    """Get screen DPI from the OS
+
+    Parameters
+    ----------
+    raise_error : bool
+        If True, raise an error if DPI could not be determined.
+
+    Returns
+    -------
+    dpi : float
+        Dots per inch of the primary screen.
+    """
+    try:
+        user32.SetProcessDPIAware()
+    except AttributeError:
+        pass  # not present on XP
+    dc = user32.GetDC(0)
+    h_size = gdi32.GetDeviceCaps(dc, HORZSIZE)
+    v_size = gdi32.GetDeviceCaps(dc, VERTSIZE)
+    h_res = gdi32.GetDeviceCaps(dc, HORZRES)
+    v_res = gdi32.GetDeviceCaps(dc, VERTRES)
+    user32.ReleaseDC(None, dc)
+    return (h_res/float(h_size) + v_res/float(v_size)) * 0.5 * 25.4
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/util/dpi/tests/__init__.py
similarity index 100%
copy from vispy/scene/shaders/tests/__init__.py
copy to vispy/util/dpi/tests/__init__.py
diff --git a/vispy/util/dpi/tests/test_dpi.py b/vispy/util/dpi/tests/test_dpi.py
new file mode 100644
index 0000000..22788d4
--- /dev/null
+++ b/vispy/util/dpi/tests/test_dpi.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from vispy.util.dpi import get_dpi
+from vispy.testing import run_tests_if_main
+
+
+def test_dpi():
+    """Test dpi support"""
+    dpi = get_dpi()
+    assert dpi > 0.
+    assert isinstance(dpi, float)
+
+
+run_tests_if_main()
diff --git a/vispy/util/event.py b/vispy/util/event.py
index 8985a28..ad2aa41 100644
--- a/vispy/util/event.py
+++ b/vispy/util/event.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -44,11 +44,11 @@ class Event(object):
        String indicating the event type (e.g. mouse_press, key_release)
     native : object (optional)
        The native GUI event object
-    **kwds : keyword arguments
+    **kwargs : keyword arguments
         All extra keyword arguments become attributes of the event object.
     """
 
-    def __init__(self, type, native=None, **kwds):
+    def __init__(self, type, native=None, **kwargs):
         # stack of all sources this event has been emitted through
         self._sources = []
         self._handled = False
@@ -56,7 +56,7 @@ class Event(object):
         # Store args
         self._type = type
         self._native = native
-        for k, v in kwds.items():
+        for k, v in kwargs.items():
             setattr(self, k, v)
 
     @property
@@ -141,6 +141,10 @@ class Event(object):
         finally:
             _event_repr_depth -= 1
 
+    def __str__(self):
+        """Shorter string representation"""
+        return self.__class__.__name__
+
 _event_repr_depth = 0
 
 
@@ -268,7 +272,8 @@ class EventEmitter(object):
         callback : function | tuple
             *callback* may be either a callable object or a tuple
             (object, attr_name) where object.attr_name will point to a
-            callable object.
+            callable object. Note that only a weak reference to ``object``
+            will be kept.
         ref : bool | str
             Reference used to identify the callback in ``before``/``after``.
             If True, the callback ref will automatically determined (see
@@ -311,6 +316,9 @@ class EventEmitter(object):
         callback_refs = self.callback_refs
         if callback in callbacks:
             return
+        # always use a weak ref
+        if isinstance(callback, tuple):
+            callback = (weakref.ref(callback[0]),) + callback[1:]
         # deal with the ref
         if isinstance(ref, bool):
             if ref:
@@ -373,13 +381,15 @@ class EventEmitter(object):
             self._callbacks = []
             self._callback_refs = []
         else:
+            if isinstance(callback, tuple):
+                callback = (weakref.ref(callback[0]),) + callback[1:]
             if callback in self._callbacks:
                 idx = self._callbacks.index(callback)
                 self._callbacks.pop(idx)
                 self._callback_refs.pop(idx)
 
-    def __call__(self, *args, **kwds):
-        """ __call__(**kwds)
+    def __call__(self, *args, **kwargs):
+        """ __call__(**kwargs)
         Invoke all callbacks for this emitter.
 
         Emit a new event object, created with the given keyword
@@ -398,23 +408,31 @@ class EventEmitter(object):
         (notably, via Event.handled) but also requires that callbacks
         be careful not to inadvertently modify the Event.
         """
+        # This is a VERY highly used method; must be fast!
+        blocked = self._blocked
         if self._emitting:
             raise RuntimeError('EventEmitter loop detected!')
 
         # create / massage event as needed
-        event = self._prepare_event(*args, **kwds)
+        event = self._prepare_event(*args, **kwargs)
 
         # Add our source to the event; remove it after all callbacks have been
         # invoked.
         event._push_source(self.source)
         self._emitting = True
         try:
-            if self.blocked():
+            if blocked.get(None, 0) > 0:  # this is the same as self.blocked()
                 return event
 
             for cb in self._callbacks:
-                if self.blocked(cb):
+                if blocked.get(cb, 0) > 0:
                     continue
+                
+                if isinstance(cb, tuple):
+                    cb = getattr(cb[0](), cb[1], None)
+                    if cb is None:
+                        continue
+                
                 self._invoke_callback(cb, event)
                 if event.blocked:
                     break
@@ -426,36 +444,24 @@ class EventEmitter(object):
         return event
 
     def _invoke_callback(self, cb, event):
-        if isinstance(cb, tuple):
-            cb = getattr(cb[0], cb[1], None)
-            if cb is None:
-                return
-
         try:
             cb(event)
         except Exception:
-            # get traceback and store (so we can do postmortem
-            # debugging)
-            #import pdb
-            #import PyQt4
-            #PyQt4.QtCore.pyqtRemoveInputHook()
-            #pdb.post_mortem()
-            #PyQt4.QtCore.pyqtRestoreInputHook()
             _handle_exception(self.ignore_callback_errors,
                               self.print_callback_errors,
                               self, cb_event=(cb, event))
 
-    def _prepare_event(self, *args, **kwds):
+    def _prepare_event(self, *args, **kwargs):
         # When emitting, this method is called to create or otherwise alter
         # an event before it is sent to callbacks. Subclasses may extend
         # this method to make custom modifications to the event.
-        if len(args) == 1 and not kwds and isinstance(args[0], Event):
+        if len(args) == 1 and not kwargs and isinstance(args[0], Event):
             event = args[0]
             # Ensure that the given event matches what we want to emit
             assert isinstance(event, self.event_class)
         elif not args:
             args = self.default_args.copy()
-            args.update(kwds)
+            args.update(kwargs)
             event = self.event_class(**args)
         else:
             raise ValueError("Event emitters can be called with an Event "
@@ -513,14 +519,14 @@ class WarningEmitter(EventEmitter):
     EventEmitter subclass used to allow deprecated events to be used with a
     warning message.
     """
-    def __init__(self, message, *args, **kwds):
+    def __init__(self, message, *args, **kwargs):
         self._message = message
         self._warned = False
-        EventEmitter.__init__(self, *args, **kwds)
+        EventEmitter.__init__(self, *args, **kwargs)
 
-    def connect(self, cb, *args, **kwds):
+    def connect(self, cb, *args, **kwargs):
         self._warn(cb)
-        return EventEmitter.connect(self, cb, *args, **kwds)
+        return EventEmitter.connect(self, cb, *args, **kwargs)
 
     def _invoke_callback(self, cb, event):
         self._warn(cb)
@@ -601,7 +607,7 @@ class EmitterGroup(EventEmitter):
         """
         self.add(**{name: emitter})
 
-    def add(self, auto_connect=None, **kwds):
+    def add(self, auto_connect=None, **kwargs):
         """ Add one or more EventEmitter instances to this emitter group.
         Each keyword argument may be specified as either an EventEmitter
         instance or an Event subclass, in which case an EventEmitter will be
@@ -621,7 +627,7 @@ class EmitterGroup(EventEmitter):
             auto_connect = self.auto_connect
 
         # check all names before adding anything
-        for name in kwds:
+        for name in kwargs:
             if name in self._emitters:
                 raise ValueError(
                     "EmitterGroup already has an emitter named '%s'" %
@@ -632,7 +638,7 @@ class EmitterGroup(EventEmitter):
                                  % name)
 
         # add each emitter specified in the keyword arguments
-        for name, emitter in kwds.items():
+        for name, emitter in kwargs.items():
             if emitter is None:
                 emitter = Event
 
@@ -723,6 +729,19 @@ class EmitterGroup(EventEmitter):
 
         self._emitters_connected = connect
 
+    @property
+    def ignore_callback_errors(self):
+        return super(EventEmitter, self).ignore_callback_errors
+
+    @ignore_callback_errors.setter
+    def ignore_callback_errors(self, ignore):
+        EventEmitter.ignore_callback_errors.fset(self, ignore)
+        for emitter in self._emitters.values():
+            if isinstance(emitter, EventEmitter):
+                emitter.ignore_callback_errors = ignore
+            elif isinstance(emitter, EmitterGroup):
+                emitter.ignore_callback_errors_all(ignore)
+
 
 class EventBlocker(object):
 
diff --git a/vispy/util/fetching.py b/vispy/util/fetching.py
index 4bdc506..8f2a2e8 100644
--- a/vispy/util/fetching.py
+++ b/vispy/util/fetching.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """Data downloading and reading functions
@@ -10,8 +10,10 @@ import os
 from os import path as op
 import sys
 import shutil
+import time
 
 from ..ext.six.moves import urllib
+from ..ext.six import string_types
 from ..util.config import config
 
 
@@ -30,9 +32,11 @@ def load_data_file(fname, directory=None, force_download=False):
     directory : str | None
         Directory to use to save the file. By default, the vispy
         configuration directory is used.
-    force_download : bool
+    force_download : bool | str
         If True, the file will be downloaded even if a local copy exists
-        (and this copy will be overwritten).
+        (and this copy will be overwritten). Can also be a YYYY-MM-DD date
+        to ensure a file is up-to-date (modified date of a file on disk,
+        if present, is checked).
 
     Returns
     -------
@@ -48,8 +52,16 @@ def load_data_file(fname, directory=None, force_download=False):
                              'so directory must be supplied')
 
     fname = op.join(directory, op.normcase(fname))  # convert to native
-    if op.isfile(fname) and not force_download:  # we're done
-        return fname
+    if op.isfile(fname):
+        if not force_download:  # we're done
+            return fname
+        if isinstance(force_download, string_types):
+            ntime = time.strptime(force_download, '%Y-%m-%d')
+            ftime = time.gmtime(op.getctime(fname))
+            if ftime >= ntime:
+                return fname
+            else:
+                print('File older than %s, updating...' % force_download)
     if not op.isdir(op.dirname(fname)):
         os.makedirs(op.abspath(op.dirname(fname)))
     # let's go get the file
@@ -57,44 +69,6 @@ def load_data_file(fname, directory=None, force_download=False):
     return fname
 
 
-def get_testing_file(fname, directory=None, force_download=False):
-    """Get a standard vispy test data file
-
-    Parameters
-    ----------
-    fname : str
-        The filename on the remote ``test-data`` repository to download,
-        e.g. ``'visuals/square.png'``. These correspond to paths
-        on ``https://github.com/vispy/test-data/``.
-    directory : str | None
-        Directory to use to save the file. By default, the vispy
-        configuration directory is used.
-    force_download : bool
-        If True, the file will be downloaded even if a local copy exists
-        (and this copy will be overwritten).
-
-    Returns
-    -------
-    fname : str
-        The path to the file on the local system.
-    """
-    _url_root = 'https://github.com/vispy/test-data/raw/master/'
-    url = _url_root + fname
-    if directory is None:
-        directory = config['data_path']
-        if directory is None:
-            raise ValueError('config["data_path"] is not defined, '
-                             'so directory must be supplied')
-
-    fname = op.join(directory, op.normcase(fname))  # convert to native
-    if op.isfile(fname) and not force_download:  # we're done
-        return fname
-    if not op.isdir(op.dirname(fname)):
-        os.makedirs(op.abspath(op.dirname(fname)))
-    # let's go get the file
-    _fetch_file(url, fname)
-    return fname
-
 ###############################################################################
 # File downloading (most adapted from mne-python)
 
@@ -251,8 +225,6 @@ def _fetch_file(url, file_name, print_destination=True):
     print_destination: bool, optional
         If true, destination of where file was saved will be printed after
         download finishes.
-    resume: bool, optional
-        If true, try to resume partially downloaded files.
     """
     # Adapted from NISL:
     # https://github.com/nisl/tutorial/blob/master/nisl/datasets.py
@@ -260,14 +232,19 @@ def _fetch_file(url, file_name, print_destination=True):
     temp_file_name = file_name + ".part"
     local_file = None
     initial_size = 0
+    # Checking file size and displaying it alongside the download url
+    n_try = 3
+    for ii in range(n_try):
+        try:
+            data = urllib.request.urlopen(url, timeout=15.)
+        except Exception as e:
+            if ii == n_try - 1:
+                raise RuntimeError('Error while fetching file %s.\n'
+                                   'Dataset fetching aborted (%s)' % (url, e))
     try:
-        # Checking file size and displaying it alongside the download url
-        u = urllib.request.urlopen(url, timeout=5.)
-        file_size = int(u.headers['Content-Length'].strip())
+        file_size = int(data.headers['Content-Length'].strip())
         print('Downloading data from %s (%s)' % (url, sizeof_fmt(file_size)))
-        # Downloading data (can be extended to resume if need be)
         local_file = open(temp_file_name, "wb")
-        data = urllib.request.urlopen(url, timeout=5.)
         _chunk_read(data, local_file, initial_size=initial_size)
         # temp file must be closed prior to the move
         if not local_file.closed:
diff --git a/vispy/util/filter.py b/vispy/util/filter.py
index c9bb7a9..87ae7aa 100644
--- a/vispy/util/filter.py
+++ b/vispy/util/filter.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
diff --git a/vispy/util/fonts/__init__.py b/vispy/util/fonts/__init__.py
index 6491ff5..e668f58 100644
--- a/vispy/util/fonts/__init__.py
+++ b/vispy/util/fonts/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 """
diff --git a/vispy/util/fonts/_freetype.py b/vispy/util/fonts/_freetype.py
index b47d665..3b33d0b 100644
--- a/vispy/util/fonts/_freetype.py
+++ b/vispy/util/fonts/_freetype.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
diff --git a/vispy/util/fonts/_quartz.py b/vispy/util/fonts/_quartz.py
index d7c862e..84499f5 100644
--- a/vispy/util/fonts/_quartz.py
+++ b/vispy/util/fonts/_quartz.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
diff --git a/vispy/util/fonts/_triage.py b/vispy/util/fonts/_triage.py
index f9c696f..ddbc93d 100644
--- a/vispy/util/fonts/_triage.py
+++ b/vispy/util/fonts/_triage.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
diff --git a/vispy/util/fonts/_vispy_fonts.py b/vispy/util/fonts/_vispy_fonts.py
index 19023b7..842a3e7 100644
--- a/vispy/util/fonts/_vispy_fonts.py
+++ b/vispy/util/fonts/_vispy_fonts.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
diff --git a/vispy/util/fonts/_win32.py b/vispy/util/fonts/_win32.py
index 8a51760..a668c47 100644
--- a/vispy/util/fonts/_win32.py
+++ b/vispy/util/fonts/_win32.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
@@ -42,6 +42,7 @@ def find_font(face, bold, italic):
     assert sizeof(metrics) >= n_byte
     assert gdi32.GetOutlineTextMetricsW(dc, n_byte, byref(metrics))
     gdi32.SelectObject(dc, original)
+    user32.ReleaseDC(None, dc)
     use_face = cast(byref(metrics, metrics.otmpFamilyName), c_wchar_p).value
     if use_face != face:
         warnings.warn('Could not find face match "%s", falling back to "%s"'
@@ -100,4 +101,5 @@ def _list_fonts():
         return 1
 
     gdi32.EnumFontFamiliesExW(dc, byref(logfont), FONTENUMPROC(enum_fun), 0, 0)
+    user32.ReleaseDC(None, dc)
     return fonts
diff --git a/vispy/util/fonts/tests/test_font.py b/vispy/util/fonts/tests/test_font.py
index 9333b93..2215dbd 100644
--- a/vispy/util/fonts/tests/test_font.py
+++ b/vispy/util/fonts/tests/test_font.py
@@ -1,12 +1,14 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
+import os
+import sys
 import numpy as np
 from nose.tools import assert_true, assert_equal
 import warnings
 
-from vispy.testing import assert_in
+from vispy.testing import assert_in, run_tests_if_main
 from vispy.util.fonts import list_fonts, _load_glyph, _vispy_fonts
 
 warnings.simplefilter('always')
@@ -29,7 +31,14 @@ def test_font_glyph():
         font_dict = dict(face=face, size=12, bold=False, italic=False)
         glyphs_dict = dict()
         chars = 'foobar^C&#'
+        if face != 'OpenSans' and os.getenv('APPVEYOR', '').lower() == 'true':
+            continue  # strange system font failure
+        if 'true' in os.getenv('TRAVIS', '') and sys.version[0] == '3':
+            continue  # as of April 2015 strange FontConfig error on Travis
         for char in chars:
             # Warning that Arial might not exist
             _load_glyph(font_dict, char, glyphs_dict)
         assert_equal(len(glyphs_dict), np.unique([c for c in chars]).size)
+
+
+run_tests_if_main()
diff --git a/vispy/util/fourier.py b/vispy/util/fourier.py
new file mode 100644
index 0000000..0f03683
--- /dev/null
+++ b/vispy/util/fourier.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+import numpy as np
+
+
+def stft(x, n_fft=1024, step=512, fs=2*np.pi, window='hann'):
+    """Compute the STFT
+
+    Parameters
+    ----------
+    x : array-like
+        1D signal to operate on. ``If len(x) < n_fft``, x will be zero-padded
+        to length ``n_fft``.
+    n_fft : int
+        Number of FFT points. Much faster for powers of two.
+    step : int | None
+        Step size between calculations. If None, ``n_fft // 2``
+        will be used.
+    fs : float
+        The sample rate of the data.
+    window : str | None
+        Window function to use. Can be ``'hann'`` for Hann window, or None
+        for no windowing.
+
+    Returns
+    -------
+    stft : ndarray
+        Spectrogram of the data, shape (n_freqs, n_steps).
+
+    See also
+    --------
+    fft_freqs
+    """
+    x = np.asarray(x, float)
+    if x.ndim != 1:
+        raise ValueError('x must be 1D')
+    if window is not None:
+        if window not in ('hann',):
+            raise ValueError('window must be "hann" or None')
+        w = np.hanning(n_fft)
+    else:
+        w = np.ones(n_fft)
+    n_fft = int(n_fft)
+    step = max(n_fft // 2, 1) if step is None else int(step)
+    fs = float(fs)
+    zero_pad = n_fft - len(x)
+    if zero_pad > 0:
+        x = np.concatenate((x, np.zeros(zero_pad, float)))
+    n_freqs = n_fft // 2 + 1
+    n_estimates = (len(x) - n_fft) // step + 1
+    result = np.empty((n_freqs, n_estimates), np.complex128)
+    for ii in range(n_estimates):
+        result[:, ii] = np.fft.rfft(w * x[ii * step:ii * step + n_fft]) / n_fft
+    return result
+
+
+def fft_freqs(n_fft, fs):
+    """Return frequencies for DFT
+
+    Parameters
+    ----------
+    n_fft : int
+        Number of points in the FFT.
+    fs : float
+        The sampling rate.
+    """
+    return np.arange(0, (n_fft // 2 + 1)) / float(n_fft) * float(fs)
diff --git a/vispy/util/keys.py b/vispy/util/keys.py
index 5ed7a92..e64482c 100644
--- a/vispy/util/keys.py
+++ b/vispy/util/keys.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """ Define constants for keys.
diff --git a/vispy/util/logs.py b/vispy/util/logs.py
index b9443a1..ad21301 100644
--- a/vispy/util/logs.py
+++ b/vispy/util/logs.py
@@ -1,15 +1,19 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
+import base64
 import logging
 import math
 import sys
 import inspect
 import re
 import traceback
+import json
 from functools import partial
 
+import numpy as np
+
 from ..ext.six import string_types
 
 
@@ -25,7 +29,9 @@ def _get_vispy_caller():
         if module.startswith('vispy'):
             line = str(record[0].f_lineno)
             func = record[3]
-            caller = "{0}:{1}({2}): ".format(module, func, line)
+            cls = record[0].f_locals.get('self', None)
+            clsname = "" if cls is None else cls.__class__.__name__ + '.' 
+            caller = "{0}:{1}{2}({3}): ".format(module, clsname, func, line)
             return caller
     return 'unknown'
 
@@ -135,7 +141,7 @@ def set_log_level(verbose, match=None, return_old=False):
         String to match. Only those messages that both contain a substring
         that regexp matches ``'match'`` (and the ``verbose`` level) will be
         displayed.
-    return_old_level : bool
+    return_old : bool
         If True, return the old verbosity level and old match.
 
     Notes
@@ -185,6 +191,8 @@ class use_log_level(object):
     ----------
     level : str
         See ``set_log_level`` for options.
+    match : str | None
+        The string to match.
     record : bool
         If True, the context manager will keep a record of the logging
         messages generated by vispy. Otherwise, an empty list will
@@ -267,7 +275,7 @@ logger.log_exception = log_exception  # make this easier to reach
 
 
 def _handle_exception(ignore_callback_errors, print_callback_errors, obj,
-                      cb_event=None, entity=None):
+                      cb_event=None, node=None):
     """Helper for prining errors in callbacks
 
     See EventEmitter._invoke_callback for a use example.
@@ -280,7 +288,7 @@ def _handle_exception(ignore_callback_errors, print_callback_errors, obj,
         cb, event = cb_event
         exp_type = 'callback'
     else:
-        exp_type = 'entity'
+        exp_type = 'node'
     type_, value, tb = sys.exc_info()
     tb = tb.tb_next  # Skip *this* frame
     sys.last_type = type_
@@ -297,7 +305,7 @@ def _handle_exception(ignore_callback_errors, print_callback_errors, obj,
             if exp_type == 'callback':
                 key = repr(cb) + repr(event)
             else:
-                key = repr(entity)
+                key = repr(node)
             if key in registry:
                 registry[key] += 1
                 if print_callback_errors == 'first':
@@ -315,14 +323,37 @@ def _handle_exception(ignore_callback_errors, print_callback_errors, obj,
         if this_print == 'full':
             logger.log_exception()
             if exp_type == 'callback':
-                logger.warning("Error invoking callback %s for "
-                               "event: %s" % (cb, event))
-            else:  # == 'entity':
-                logger.warning("Error drawing entity %s" % entity)
+                logger.error("Invoking %s for %s" % (cb, event))
+            else:  # == 'node':
+                logger.error("Drawing node %s" % node)
         elif this_print is not None:
             if exp_type == 'callback':
-                logger.warning("Error invoking callback %s repeat %s"
-                               % (cb, this_print))
-            else:  # == 'entity':
-                logger.warning("Error drawing entity %s repeat %s"
-                               % (entity, this_print))
+                logger.error("Invoking %s repeat %s"
+                             % (cb, this_print))
+            else:  # == 'node':
+                logger.error("Drawing node %s repeat %s"
+                             % (node, this_print))
+
+
+def _serialize_buffer(buffer, array_serialization=None):
+    """Serialize a NumPy array."""
+    if array_serialization == 'binary':
+        # WARNING: in NumPy 1.9, tostring() has been renamed to tobytes()
+        # but tostring() is still here for now for backward compatibility.
+        return buffer.ravel().tostring()
+    elif array_serialization == 'base64':
+        return {'storage_type': 'base64',
+                'buffer': base64.b64encode(buffer).decode('ascii')
+                }
+    raise ValueError("The array serialization method should be 'binary' or "
+                     "'base64'.")
+
+
+class NumPyJSONEncoder(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, np.ndarray):
+            return _serialize_buffer(obj, array_serialization='base64')
+        elif isinstance(obj, np.generic):
+            return obj.item()
+
+        return json.JSONEncoder.default(self, obj)
diff --git a/vispy/util/profiler.py b/vispy/util/profiler.py
new file mode 100644
index 0000000..c414276
--- /dev/null
+++ b/vispy/util/profiler.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# Adapted from PyQtGraph
+import sys
+from . import ptime
+from .. import config
+
+
+class Profiler(object):
+    """Simple profiler allowing directed, hierarchical measurement of time
+    intervals.
+
+    By default, profilers are disabled.  To enable profiling, set the
+    environment variable `VISPYPROFILE` to a comma-separated list of
+    fully-qualified names of profiled functions.
+
+    Calling a profiler registers a message (defaulting to an increasing
+    counter) that contains the time elapsed since the last call.  When the
+    profiler is about to be garbage-collected, the messages are passed to the
+    outer profiler if one is running, or printed to stdout otherwise.
+
+    If `delayed` is set to False, messages are immediately printed instead.
+
+    Example:
+        def function(...):
+            profiler = Profiler()
+            ... do stuff ...
+            profiler('did stuff')
+            ... do other stuff ...
+            profiler('did other stuff')
+            # profiler is garbage-collected and flushed at function end
+
+    If this function is a method of class C, setting `VISPYPROFILE` to
+    "C.function" (without the module name) will enable this profiler.
+
+    For regular functions, use the qualified name of the function, stripping
+    only the initial "vispy.." prefix from the module.
+    """
+
+    _profilers = (config['profile'].split(",") if config['profile'] is not None
+                  else [])
+    
+    _depth = 0
+    _msgs = []
+    # set this flag to disable all or individual profilers at runtime
+    disable = False
+    
+    class DisabledProfiler(object):
+        def __init__(self, *args, **kwds):
+            pass
+        
+        def __call__(self, *args):
+            pass
+        
+        def finish(self):
+            pass
+        
+        def mark(self, msg=None):
+            pass
+        
+    _disabled_profiler = DisabledProfiler()
+        
+    def __new__(cls, msg=None, disabled='env', delayed=True):
+        """Optionally create a new profiler based on caller's qualname.
+        """
+        if (disabled is True or 
+                (disabled == 'env' and len(cls._profilers) == 0)):
+            return cls._disabled_profiler
+                        
+        # determine the qualified name of the caller function
+        caller_frame = sys._getframe(1)
+        try:
+            caller_object_type = type(caller_frame.f_locals["self"])
+        except KeyError:  # we are in a regular function
+            qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1]
+        else:  # we are in a method
+            qualifier = caller_object_type.__name__
+        func_qualname = qualifier + "." + caller_frame.f_code.co_name
+        if (disabled == 'env' and func_qualname not in cls._profilers and
+                'all' not in cls._profilers):  # don't do anything
+            return cls._disabled_profiler
+        # create an actual profiling object
+        cls._depth += 1
+        obj = super(Profiler, cls).__new__(cls)
+        obj._name = msg or func_qualname
+        obj._delayed = delayed
+        obj._mark_count = 0
+        obj._finished = False
+        obj._firstTime = obj._last_time = ptime.time()
+        obj._new_msg("> Entering " + obj._name)
+        return obj
+
+    def __call__(self, msg=None, *args):
+        """Register or print a new message with timing information.
+        """
+        if self.disable:
+            return
+        if msg is None:
+            msg = str(self._mark_count)
+        self._mark_count += 1
+        new_time = ptime.time()
+        elapsed = (new_time - self._last_time) * 1000
+        self._new_msg("  " + msg + ": %0.4f ms", *(args + (elapsed,)))
+        self._last_time = new_time
+        
+    def mark(self, msg=None):
+        self(msg)
+
+    def _new_msg(self, msg, *args):
+        msg = "  " * (self._depth - 1) + msg
+        if self._delayed:
+            self._msgs.append((msg, args))
+        else:
+            self.flush()
+            print(msg % args)
+
+    def __del__(self):
+        self.finish()
+    
+    def finish(self, msg=None):
+        """Add a final message; flush the message list if no parent profiler.
+        """
+        if self._finished or self.disable:
+            return        
+        self._finished = True
+        if msg is not None:
+            self(msg)
+        self._new_msg("< Exiting %s, total time: %0.4f ms", 
+                      self._name, (ptime.time() - self._firstTime) * 1000)
+        type(self)._depth -= 1
+        if self._depth < 1:
+            self.flush()
+        
+    def flush(self):
+        if self._msgs:
+            print("\n".join([m[0] % m[1] for m in self._msgs]))
+            type(self)._msgs = []
diff --git a/vispy/util/ptime.py b/vispy/util/ptime.py
index 093105f..5b5c0f4 100644
--- a/vispy/util/ptime.py
+++ b/vispy/util/ptime.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
diff --git a/vispy/util/quaternion.py b/vispy/util/quaternion.py
new file mode 100644
index 0000000..9f4a18e
--- /dev/null
+++ b/vispy/util/quaternion.py
@@ -0,0 +1,236 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# Based on the quaternion class in the visvis project.
+
+import numpy as np
+
+
+class Quaternion(object):
+    """ Quaternion(w=1, x=0, y=0, z=0, normalize=True)
+    
+    A quaternion is a mathematically convenient way to
+    describe rotations.
+    
+    """
+    
+    def __init__(self, w=1, x=0, y=0, z=0, normalize=True):
+        
+        self.w = float(w)
+        self.x, self.y, self.z = float(x), float(y), float(z)
+        if normalize:
+            self._normalize()
+    
+    def __repr__(self):
+        return "<Quaternion object %1.3g + %1.3gi + %1.3gj + %1.3gk>" % (
+               self.w, self.x, self.y, self.z)
+    
+    def copy(self):
+        """ Create an exact copy of this quaternion. 
+        """
+        return Quaternion(self.w, self.x, self.y, self.z, False)
+    
+    def norm(self):
+        """ Returns the norm of the quaternion
+        
+        norm = w**2 + x**2 + y**2 + z**2
+        """
+        tmp = self.w**2 + self.x**2 + self.y**2 + self.z**2
+        return tmp**0.5
+    
+    def _normalize(self):
+        """ Make the quaternion unit length.
+        """
+        # Get length
+        L = self.norm()
+        if not L:
+            raise ValueError('Quaternion cannot have 0-length.')
+        # Correct
+        self.w /= L
+        self.x /= L
+        self.y /= L
+        self.z /= L
+    
+    def normalize(self):
+        """ Returns a normalized (unit length) version of the quaternion.
+        """
+        new = self.copy()
+        new._normalize()
+        return new
+    
+    def conjugate(self):
+        """ Obtain the conjugate of the quaternion.
+        
+        This is simply the same quaternion but with the sign of the
+        imaginary (vector) parts reversed.
+        """
+        new = self.copy()
+        new.x *= -1
+        new.y *= -1
+        new.z *= -1
+        return new
+    
+    def inverse(self):
+        """ returns q.conjugate()/q.norm()**2
+        
+        So if the quaternion is unit length, it is the same
+        as the conjugate.
+        """
+        new = self.conjugate()
+        tmp = self.norm()**2
+        new.w /= tmp
+        new.x /= tmp
+        new.y /= tmp
+        new.z /= tmp
+        return new
+    
+    def exp(self):
+        """ Returns the exponent of the quaternion. 
+        (not tested)
+        """
+        
+        # Init
+        vecNorm = self.x**2 + self.y**2 + self.z**2
+        wPart = np.exp(self.w)        
+        q = Quaternion()
+        
+        # Calculate
+        q.w = wPart * np.cos(vecNorm)
+        q.x = wPart * self.x * np.sin(vecNorm) / vecNorm
+        q.y = wPart * self.y * np.sin(vecNorm) / vecNorm
+        q.z = wPart * self.z * np.sin(vecNorm) / vecNorm
+        
+        return q
+    
+    def log(self):
+        """ Returns the natural logarithm of the quaternion. 
+        (not tested)
+        """
+        
+        # Init
+        norm = self.norm()
+        vecNorm = self.x**2 + self.y**2 + self.z**2
+        tmp = self.w / norm
+        q = Quaternion()
+        
+        # Calculate
+        q.w = np.log(norm)
+        q.x = np.log(norm) * self.x * np.arccos(tmp) / vecNorm
+        q.y = np.log(norm) * self.y * np.arccos(tmp) / vecNorm
+        q.z = np.log(norm) * self.z * np.arccos(tmp) / vecNorm
+        
+        return q
+    
+    def __add__(self, q):
+        """ Add quaternions. """
+        new = self.copy()
+        new.w += q.w
+        new.x += q.x
+        new.y += q.y
+        new.z += q.z
+        return new
+    
+    def __sub__(self, q):
+        """ Subtract quaternions. """
+        new = self.copy()
+        new.w -= q.w
+        new.x -= q.x
+        new.y -= q.y
+        new.z -= q.z
+        return new
+    
+    def __mul__(self, q2):
+        """ Multiply two quaternions. """
+        new = Quaternion()
+        q1 = self       
+        new.w = q1.w*q2.w - q1.x*q2.x - q1.y*q2.y - q1.z*q2.z
+        new.x = q1.w*q2.x + q1.x*q2.w + q1.y*q2.z - q1.z*q2.y
+        new.y = q1.w*q2.y + q1.y*q2.w + q1.z*q2.x - q1.x*q2.z
+        new.z = q1.w*q2.z + q1.z*q2.w + q1.x*q2.y - q1.y*q2.x
+        return new
+    
+    def rotate_point(self, p):
+        """ Rotate a Point instance using this quaternion.
+        """
+        # Prepare 
+        p = Quaternion(0, p[0], p[1], p[2], False)  # Do not normalize!
+        q1 = self.normalize()
+        q2 = self.inverse()
+        # Apply rotation
+        r = (q1*p)*q2
+        # Make point and return        
+        return r.x, r.y, r.z
+    
+    def get_matrix(self):
+        """ Create a 4x4 homography matrix that represents the rotation
+        of the quaternion.
+        """
+        # Init matrix (remember, a matrix, not an array)
+        a = np.zeros((4, 4), dtype=np.float32)
+        w, x, y, z = self.w, self.x, self.y, self.z
+        # First row
+        a[0, 0] = - 2.0 * (y * y + z * z) + 1.0
+        a[1, 0] = + 2.0 * (x * y + z * w)
+        a[2, 0] = + 2.0 * (x * z - y * w)
+        a[3, 0] = 0.0
+        # Second row
+        a[0, 1] = + 2.0 * (x * y - z * w)
+        a[1, 1] = - 2.0 * (x * x + z * z) + 1.0
+        a[2, 1] = + 2.0 * (z * y + x * w)
+        a[3, 1] = 0.0
+        # Third row
+        a[0, 2] = + 2.0 * (x * z + y * w)
+        a[1, 2] = + 2.0 * (y * z - x * w)
+        a[2, 2] = - 2.0 * (x * x + y * y) + 1.0
+        a[3, 2] = 0.0
+        # Fourth row
+        a[0, 3] = 0.0
+        a[1, 3] = 0.0
+        a[2, 3] = 0.0
+        a[3, 3] = 1.0
+        return a
+    
+    def get_axis_angle(self):
+        """ Get the axis-angle representation of the quaternion. 
+        (The angle is in radians)
+        """
+        # Init
+        angle = 2 * np.arccos(max(min(self.w, 1.), -1.))
+        scale = (self.x**2 + self.y**2 + self.z**2)**0.5    
+        
+        # Calc axis
+        if scale:
+            ax = self.x / scale
+            ay = self.y / scale
+            az = self.z / scale
+        else:
+            # No rotation, so arbitrary axis
+            ax, ay, az = 1, 0, 0
+        # Return
+        return angle, ax, ay, az
+    
+    @classmethod
+    def create_from_axis_angle(cls, angle, ax, ay, az, degrees=False):
+        """ Classmethod to create a quaternion from an axis-angle representation. 
+        (angle should be in radians).
+        """
+        if degrees:
+            angle = np.radians(angle)
+        while angle < 0:
+            angle += np.pi*2
+        angle2 = angle/2.0
+        sinang2 = np.sin(angle2)
+        return Quaternion(np.cos(angle2), ax*sinang2, ay*sinang2, az*sinang2)
+    
+    @classmethod
+    def create_from_euler_angles(cls, rx, ry, rz, degrees=False):
+        """ Classmethod to create a quaternion given the euler angles.
+        """
+        if degrees:
+            rx, ry, rz = np.radians([rx, ry, rz])
+        # Obtain quaternions
+        qx = Quaternion(np.cos(rx/2), 0, 0, np.sin(rx/2))
+        qy = Quaternion(np.cos(ry/2), 0, np.sin(ry/2), 0)
+        qz = Quaternion(np.cos(rz/2), np.sin(rz/2), 0, 0)
+        # Almost done
+        return qx*qy*qz   
diff --git a/vispy/util/svg/__init__.py b/vispy/util/svg/__init__.py
new file mode 100644
index 0000000..24a07da
--- /dev/null
+++ b/vispy/util/svg/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+from . svg import SVG
+from . path import Path  # noqa
+from . base import namespace
+from xml.etree import ElementTree
+
+
+def Document(filename):
+    tree = ElementTree.parse(filename)
+    root = tree.getroot()
+    if root.tag != namespace + 'svg':
+        text = 'File "%s" does not seem to be a valid SVG file' % filename
+        raise TypeError(text)
+    return SVG(root)
diff --git a/vispy/util/svg/base.py b/vispy/util/svg/base.py
new file mode 100644
index 0000000..d281bd0
--- /dev/null
+++ b/vispy/util/svg/base.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+
+namespace = '{http://www.w3.org/2000/svg}'
+dpi = 90
+units = {
+    None: 1,           # Default unit (same as pixel)
+    'px': 1,           # px: pixel. Default SVG unit
+    'em': 10,          # 1 em = 10 px FIXME
+    'ex': 5,           # 1 ex =  5 px FIXME
+    'in': dpi,          # 1 in = 96 px
+    'cm': dpi / 2.54,   # 1 cm = 1/2.54 in
+    'mm': dpi / 25.4,   # 1 mm = 1/25.4 in
+    'pt': dpi / 72.0,   # 1 pt = 1/72 in
+    'pc': dpi / 6.0,    # 1 pc = 1/6 in
+    '%':   1 / 100.0   # 1 percent
+}
diff --git a/vispy/util/svg/color.py b/vispy/util/svg/color.py
new file mode 100644
index 0000000..5a23520
--- /dev/null
+++ b/vispy/util/svg/color.py
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+# See <http://www.w3.org/TR/SVG/types.html#ColorKeywords>
+_keyword_colors = {
+    "aliceblue":            (240, 248, 255),
+    "antiquewhite":         (250, 235, 215),
+    "aqua":                 (0, 255, 255),
+    "aquamarine":           (127, 255, 212),
+    "azure":                (240, 255, 255),
+    "beige":                (245, 245, 220),
+    "bisque":               (255, 228, 196),
+    "black":                (0,   0,   0),
+    "blanchedalmond":       (255, 235, 205),
+    "blue":                 (0,   0, 255),
+    "blueviolet":           (138,  43, 226),
+    "brown":                (165,  42,  42),
+    "burlywood":            (222, 184, 135),
+    "cadetblue":            (95, 158, 160),
+    "chartreuse":           (127, 255,   0),
+    "chocolate":            (210, 105,  30),
+    "coral":                (255, 127,  80),
+    "cornflowerblue":       (100, 149, 237),
+    "cornsilk":             (255, 248, 220),
+    "crimson":              (220,  20,  60),
+    "cyan":                 (0, 255, 255),
+    "darkblue":             (0,   0, 139),
+    "darkcyan":             (0, 139, 139),
+    "darkgoldenrod":        (184, 134,  11),
+    "darkgray":             (169, 169, 169),
+    "darkgreen":            (0, 100,   0),
+    "darkgrey":             (169, 169, 169),
+    "darkkhaki":            (189, 183, 107),
+    "darkmagenta":          (139,   0, 139),
+    "darkolivegreen":       (85, 107,  47),
+    "darkorange":           (255, 140,   0),
+    "darkorchid":           (153,  50, 204),
+    "darkred":              (139,   0,   0),
+    "darksalmon":           (233, 150, 122),
+    "darkseagreen":         (143, 188, 143),
+    "darkslateblue":        (72,  61, 139),
+    "darkslategray":        (47,  79,  79),
+    "darkslategrey":        (47,  79,  79),
+    "darkturquoise":        (0, 206, 209),
+    "darkviolet":           (148,   0, 211),
+    "deeppink":             (255,  20, 147),
+    "deepskyblue":          (0, 191, 255),
+    "dimgray":              (105, 105, 105),
+    "dimgrey":              (105, 105, 105),
+    "dodgerblue":           (30, 144, 255),
+    "firebrick":            (178,  34,  34),
+    "floralwhite":          (255, 250, 240),
+    "forestgreen":          (34, 139,  34),
+    "fuchsia":              (255,   0, 255),
+    "gainsboro":            (220, 220, 220),
+    "ghostwhite":           (248, 248, 255),
+    "gold":                 (255, 215,   0),
+    "goldenrod":            (218, 165,  32),
+    "gray":                 (128, 128, 128),
+    "grey":                 (128, 128, 128),
+    "green":                (0, 128,   0),
+    "greenyellow":          (173, 255,  47),
+    "honeydew":             (240, 255, 240),
+    "hotpink":              (255, 105, 180),
+    "indianred":            (205,  92,  92),
+    "indigo":               (75,   0, 130),
+    "ivory":                (255, 255, 240),
+    "khaki":                (240, 230, 140),
+    "lavender":             (230, 230, 250),
+    "lavenderblush":        (255, 240, 245),
+    "lawngreen":            (124, 252,   0),
+    "lemonchiffon":         (255, 250, 205),
+    "lightblue":            (173, 216, 230),
+    "lightcoral":           (240, 128, 128),
+    "lightcyan":            (224, 255, 255),
+    "lightgoldenrodyellow": (250, 250, 210),
+    "lightgray":            (211, 211, 211),
+    "lightgreen":           (144, 238, 144),
+    "lightgrey":            (211, 211, 211),
+    "lightpink":            (255, 182, 193),
+    "lightsalmon":          (255, 160, 122),
+    "lightseagreen":        (32, 178, 170),
+    "lightskyblue":         (135, 206, 250),
+    "lightslategray":       (119, 136, 153),
+    "lightslategrey":       (119, 136, 153),
+    "lightsteelblue":       (176, 196, 222),
+    "lightyellow":          (255, 255, 224),
+    "lime":                 (0, 255,   0),
+    "limegreen":            (50, 205,  50),
+    "linen":                (250, 240, 230),
+    "magenta":              (255,   0, 255),
+    "maroon":               (128,   0,   0),
+    "mediumaquamarine":     (102, 205, 170),
+    "mediumblue":           (0,   0, 205),
+    "mediumorchid":         (186,  85, 211),
+    "mediumpurple":         (147, 112, 219),
+    "mediumseagreen":       (60, 179, 113),
+    "mediumslateblue":      (123, 104, 238),
+    "mediumspringgreen":    (0, 250, 154),
+    "mediumturquoise":      (72, 209, 204),
+    "mediumvioletred":      (199,  21, 133),
+    "midnightblue":         (25,  25, 112),
+    "mintcream":            (245, 255, 250),
+    "mistyrose":            (255, 228, 225),
+    "moccasin":             (255, 228, 181),
+    "navajowhite":          (255, 222, 173),
+    "navy":                 (0,   0, 128),
+    "oldlace":              (253, 245, 230),
+    "olive":                (128, 128,   0),
+    "olivedrab":            (107, 142,  35),
+    "orange":               (255, 165,   0),
+    "orangered":            (255,  69,   0),
+    "orchid":               (218, 112, 214),
+    "palegoldenrod":        (238, 232, 170),
+    "palegreen":            (152, 251, 152),
+    "paleturquoise":        (175, 238, 238),
+    "palevioletred":        (219, 112, 147),
+    "papayawhip":           (255, 239, 213),
+    "peachpuff":            (255, 218, 185),
+    "peru":                 (205, 133,  63),
+    "pink":                 (255, 192, 203),
+    "plum":                 (221, 160, 221),
+    "powderblue":           (176, 224, 230),
+    "purple":               (128,   0, 128),
+    "red":                  (255,   0,   0),
+    "rosybrown":            (188, 143, 143),
+    "royalblue":            (65, 105, 225),
+    "saddlebrown":          (139,  69,  19),
+    "salmon":               (250, 128, 114),
+    "sandybrown":           (244, 164,  96),
+    "seagreen":             (46, 139,  87),
+    "seashell":             (255, 245, 238),
+    "sienna":               (160,  82,  45),
+    "silver":               (192, 192, 192),
+    "skyblue":              (135, 206, 235),
+    "slateblue":            (106,  90, 205),
+    "slategray":            (112, 128, 144),
+    "slategrey":            (112, 128, 144),
+    "snow":                 (255, 250, 250),
+    "springgreen":          (0, 255, 127),
+    "steelblue":            (70, 130, 180),
+    "tan":                  (210, 180, 140),
+    "teal":                 (0, 128, 128),
+    "thistle":              (216, 191, 216),
+    "tomato":               (255,  99,  71),
+    "turquoise":            (64, 224, 208),
+    "violet":               (238, 130, 238),
+    "wheat":                (245, 222, 179),
+    "white":                (255, 255, 255),
+    "whitesmoke":           (245, 245, 245),
+    "yellow":               (255, 255,   0),
+    "yellowgreen":          (154, 205,  50)}
+
+
+_NUMERALS = '0123456789abcdefABCDEF'
+
+
+_HEXDEC = {v: int(v, 16)
+           for v in (x + y for x in _NUMERALS for y in _NUMERALS)}
+
+
+def _rgb(triplet):
+    return _HEXDEC[triplet[0:2]], _HEXDEC[triplet[2:4]], _HEXDEC[triplet[4:6]]
+
+
+class Color(object):
+
+    def __init__(self, content):
+
+        color = content.strip()
+        if color.startswith("#"):
+            rgb = color[1:]
+            if len(rgb) == 3:
+                r, g, b = tuple(ord((c + c).decode('hex')) for c in rgb)
+            else:
+                # r,g,b = tuple(ord(c) for c in rgb.decode('hex'))
+                r, g, b = tuple(c for c in _rgb(rgb))
+        elif color.startswith("rgb("):
+            rgb = color[4:-1]
+            r, g, b = [value.strip() for value in rgb.split(',')]
+            if r.endswith("%"):
+                r = 255 * int(r[:-1]) // 100
+            else:
+                r = int(r)
+            if g.endswith("%"):
+                g = 255 * int(g[:-1]) // 100
+            else:
+                g = int(r)
+            if b.endswith("%"):
+                b = 255 * int(b[:-1]) // 100
+            else:
+                b = int(r)
+        elif color in _keyword_colors:
+            r, g, b = _keyword_colors[color]
+        else:
+            # text = "Unknown color (%s)" % color
+            r, g, b = 0, 0, 0
+        self._rgb = r / 255., g / 255., b / 255.
+
+    @property
+    def rgb(self):
+        r, g, b = self._rgb
+        return r, g, b
+
+    @property
+    def rgba(self):
+        r, g, b = self._rgb
+        return r, g, b, 1
+
+    def __repr__(self):
+        r, g, b = self._rgb
+        r, g, b = int(r * 255), int(g * 255), int(b * 255)
+        return "#%02x%02x%02x" % (r, g, b)
diff --git a/vispy/util/svg/element.py b/vispy/util/svg/element.py
new file mode 100644
index 0000000..d8bfa69
--- /dev/null
+++ b/vispy/util/svg/element.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+import copy
+from . style import Style
+
+namespace = '{http://www.w3.org/2000/svg}'
+
+
+class Element(object):
+
+    """ Generic SVG element """
+
+    def __init__(self, content=None, parent=None):
+        self._parent = parent
+        self._id = hex(id(self))
+        self._style = Style()
+        self._computed_style = Style()
+
+        if isinstance(content, str):
+            return
+
+        self._id = content.get('id', self._id)
+        self._style.update(content.get("style", None))
+        self._computed_style = Style()
+        if parent and parent.style:
+            self._computed_style = copy.copy(parent.style)
+            self._computed_style.update(content.get("style", None))
+
+    @property
+    def root(self):
+        if self._parent:
+            return self._parent.root
+        return self
+
+    @property
+    def parent(self):
+        if self._parent:
+            return self._parent
+        return None
+
+    @property
+    def style(self):
+        return self._computed_style
+
+    @property
+    def viewport(self):
+        if self._parent:
+            return self._parent.viewport
+        return None
diff --git a/vispy/util/svg/geometry.py b/vispy/util/svg/geometry.py
new file mode 100644
index 0000000..833d466
--- /dev/null
+++ b/vispy/util/svg/geometry.py
@@ -0,0 +1,470 @@
+# ----------------------------------------------------------------------------
+#  Anti-Grain Geometry (AGG) - Version 2.5
+#  A high quality rendering engine for C++
+#  Copyright (C) 2002-2006 Maxim Shemanarev
+#  Contact: mcseem at antigrain.com
+#           mcseemagg at yahoo.com
+#           http://antigrain.com
+#
+#  AGG is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  AGG is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with AGG; if not, write to the Free Software
+#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+#  MA 02110-1301, USA.
+# ----------------------------------------------------------------------------
+#
+# Python translation by Nicolas P. Rougier
+# Copyright (C) 2013 Nicolas P. Rougier. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY NICOLAS P. ROUGIER ''AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL NICOLAS P. ROUGIER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and documentation are
+# those of the authors and should not be interpreted as representing official
+# policies, either expressed or implied, of Nicolas P. Rougier.
+#
+# ----------------------------------------------------------------------------
+import math
+
+
+curve_distance_epsilon = 1e-30
+curve_collinearity_epsilon = 1e-30
+curve_angle_tolerance_epsilon = 0.01
+curve_recursion_limit = 32
+m_cusp_limit = 0.0
+m_angle_tolerance = 10 * math.pi / 180.0
+m_approximation_scale = 1.0
+m_distance_tolerance_square = (0.5 / m_approximation_scale)**2
+epsilon = 1e-10
+
+
+def calc_sq_distance(x1, y1, x2, y2):
+    dx = x2 - x1
+    dy = y2 - y1
+    return dx * dx + dy * dy
+
+
+def quadratic_recursive(points, x1, y1, x2, y2, x3, y3, level=0):
+    if level > curve_recursion_limit:
+        return
+
+    # Calculate all the mid-points of the line segments
+    # -------------------------------------------------
+    x12 = (x1 + x2) / 2.
+    y12 = (y1 + y2) / 2.
+    x23 = (x2 + x3) / 2.
+    y23 = (y2 + y3) / 2.
+    x123 = (x12 + x23) / 2.
+    y123 = (y12 + y23) / 2.
+
+    dx = x3 - x1
+    dy = y3 - y1
+    d = math.fabs((x2 - x3) * dy - (y2 - y3) * dx)
+
+    if d > curve_collinearity_epsilon:
+        # Regular case
+        # ------------
+        if d * d <= m_distance_tolerance_square * (dx * dx + dy * dy):
+            # If the curvature doesn't exceed the distance_tolerance value
+            # we tend to finish subdivisions.
+            if m_angle_tolerance < curve_angle_tolerance_epsilon:
+                points.append((x123, y123))
+                return
+
+            # Angle & Cusp Condition
+            da = math.fabs(
+                math.atan2(y3 - y2, x3 - x2) - math.atan2(y2 - y1, x2 - x1))
+            if da >= math.pi:
+                da = 2 * math.pi - da
+
+            if da < m_angle_tolerance:
+                # Finally we can stop the recursion
+                points.append((x123, y123))
+                return
+    else:
+        # Collinear case
+        # --------------
+        da = dx * dx + dy * dy
+        if da == 0:
+            d = calc_sq_distance(x1, y1, x2, y2)
+        else:
+            d = ((x2 - x1) * dx + (y2 - y1) * dy) / da
+            if d > 0 and d < 1:
+                # Simple collinear case, 1---2---3, we can leave just two
+                # endpoints
+                return
+            if(d <= 0):
+                d = calc_sq_distance(x2, y2, x1, y1)
+            elif d >= 1:
+                d = calc_sq_distance(x2, y2, x3, y3)
+            else:
+                d = calc_sq_distance(x2, y2, x1 + d * dx, y1 + d * dy)
+
+        if d < m_distance_tolerance_square:
+            points.append((x2, y2))
+            return
+
+    # Continue subdivision
+    # --------------------
+    quadratic_recursive(points, x1, y1, x12, y12, x123, y123, level + 1)
+    quadratic_recursive(points, x123, y123, x23, y23, x3, y3, level + 1)
+
+
+def cubic_recursive(points, x1, y1, x2, y2, x3, y3, x4, y4, level=0):
+    if level > curve_recursion_limit:
+        return
+
+    # Calculate all the mid-points of the line segments
+    # -------------------------------------------------
+    x12 = (x1 + x2) / 2.
+    y12 = (y1 + y2) / 2.
+    x23 = (x2 + x3) / 2.
+    y23 = (y2 + y3) / 2.
+    x34 = (x3 + x4) / 2.
+    y34 = (y3 + y4) / 2.
+    x123 = (x12 + x23) / 2.
+    y123 = (y12 + y23) / 2.
+    x234 = (x23 + x34) / 2.
+    y234 = (y23 + y34) / 2.
+    x1234 = (x123 + x234) / 2.
+    y1234 = (y123 + y234) / 2.
+
+    # Try to approximate the full cubic curve by a single straight line
+    # -----------------------------------------------------------------
+    dx = x4 - x1
+    dy = y4 - y1
+    d2 = math.fabs(((x2 - x4) * dy - (y2 - y4) * dx))
+    d3 = math.fabs(((x3 - x4) * dy - (y3 - y4) * dx))
+
+    s = int((d2 > curve_collinearity_epsilon) << 1) + \
+        int(d3 > curve_collinearity_epsilon)
+
+    if s == 0:
+        # All collinear OR p1==p4
+        # ----------------------
+        k = dx * dx + dy * dy
+        if k == 0:
+            d2 = calc_sq_distance(x1, y1, x2, y2)
+            d3 = calc_sq_distance(x4, y4, x3, y3)
+
+        else:
+            k = 1. / k
+            da1 = x2 - x1
+            da2 = y2 - y1
+            d2 = k * (da1 * dx + da2 * dy)
+            da1 = x3 - x1
+            da2 = y3 - y1
+            d3 = k * (da1 * dx + da2 * dy)
+            if d2 > 0 and d2 < 1 and d3 > 0 and d3 < 1:
+                # Simple collinear case, 1---2---3---4
+                # We can leave just two endpoints
+                return
+
+            if d2 <= 0:
+                d2 = calc_sq_distance(x2, y2, x1, y1)
+            elif d2 >= 1:
+                d2 = calc_sq_distance(x2, y2, x4, y4)
+            else:
+                d2 = calc_sq_distance(x2, y2, x1 + d2 * dx, y1 + d2 * dy)
+
+            if d3 <= 0:
+                d3 = calc_sq_distance(x3, y3, x1, y1)
+            elif d3 >= 1:
+                d3 = calc_sq_distance(x3, y3, x4, y4)
+            else:
+                d3 = calc_sq_distance(x3, y3, x1 + d3 * dx, y1 + d3 * dy)
+
+        if d2 > d3:
+            if d2 < m_distance_tolerance_square:
+                points.append((x2, y2))
+                return
+        else:
+            if d3 < m_distance_tolerance_square:
+                points.append((x3, y3))
+                return
+
+    elif s == 1:
+        # p1,p2,p4 are collinear, p3 is significant
+        # -----------------------------------------
+        if d3 * d3 <= m_distance_tolerance_square * (dx * dx + dy * dy):
+            if m_angle_tolerance < curve_angle_tolerance_epsilon:
+                points.append((x23, y23))
+                return
+
+            # Angle Condition
+            # ---------------
+            da1 = math.fabs(
+                math.atan2(y4 - y3, x4 - x3) - math.atan2(y3 - y2, x3 - x2))
+            if da1 >= math.pi:
+                da1 = 2 * math.pi - da1
+
+            if da1 < m_angle_tolerance:
+                points.extend([(x2, y2), (x3, y3)])
+                return
+
+            if m_cusp_limit != 0.0:
+                if da1 > m_cusp_limit:
+                    points.append((x3, y3))
+                    return
+
+    elif s == 2:
+        # p1,p3,p4 are collinear, p2 is significant
+        # -----------------------------------------
+        if d2 * d2 <= m_distance_tolerance_square * (dx * dx + dy * dy):
+            if m_angle_tolerance < curve_angle_tolerance_epsilon:
+                points.append((x23, y23))
+                return
+
+            # Angle Condition
+            # ---------------
+            da1 = math.fabs(
+                math.atan2(y3 - y2, x3 - x2) - math.atan2(y2 - y1, x2 - x1))
+            if da1 >= math.pi:
+                da1 = 2 * math.pi - da1
+
+            if da1 < m_angle_tolerance:
+                points.extend([(x2, y2), (x3, y3)])
+                return
+
+            if m_cusp_limit != 0.0:
+                if da1 > m_cusp_limit:
+                    points.append((x2, y2))
+                    return
+
+    elif s == 3:
+        # Regular case
+        # ------------
+        if (d2 + d3) * (d2 + d3) <= m_distance_tolerance_square * (dx * dx + dy * dy):  # noqa
+            # If the curvature doesn't exceed the distance_tolerance value
+            # we tend to finish subdivisions.
+
+            if m_angle_tolerance < curve_angle_tolerance_epsilon:
+                points.append((x23, y23))
+                return
+
+            # Angle & Cusp Condition
+            # ----------------------
+            k = math.atan2(y3 - y2, x3 - x2)
+            da1 = math.fabs(k - math.atan2(y2 - y1, x2 - x1))
+            da2 = math.fabs(math.atan2(y4 - y3, x4 - x3) - k)
+            if da1 >= math.pi:
+                da1 = 2 * math.pi - da1
+            if da2 >= math.pi:
+                da2 = 2 * math.pi - da2
+
+            if da1 + da2 < m_angle_tolerance:
+                # Finally we can stop the recursion
+                # ---------------------------------
+                points.append((x23, y23))
+                return
+
+            if m_cusp_limit != 0.0:
+                if da1 > m_cusp_limit:
+                    points.append((x2, y2))
+                    return
+
+                if da2 > m_cusp_limit:
+                    points.append((x3, y3))
+                    return
+
+    # Continue subdivision
+    # --------------------
+    cubic_recursive(
+        points, x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1)
+    cubic_recursive(
+        points, x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1)
+
+
+def quadratic(p1, p2, p3):
+    x1, y1 = p1
+    x2, y2 = p2
+    x3, y3 = p3
+    points = []
+    quadratic_recursive(points, x1, y1, x2, y2, x3, y3)
+
+    dx, dy = points[0][0] - x1, points[0][1] - y1
+    if (dx * dx + dy * dy) > epsilon:
+        points.insert(0, (x1, y1))
+
+    dx, dy = points[-1][0] - x3, points[-1][1] - y3
+    if (dx * dx + dy * dy) > epsilon:
+        points.append((x3, y3))
+
+    return points
+
+
+def cubic(p1, p2, p3, p4):
+    x1, y1 = p1
+    x2, y2 = p2
+    x3, y3 = p3
+    x4, y4 = p4
+    points = []
+    cubic_recursive(points, x1, y1, x2, y2, x3, y3, x4, y4)
+
+    dx, dy = points[0][0] - x1, points[0][1] - y1
+    if (dx * dx + dy * dy) > epsilon:
+        points.insert(0, (x1, y1))
+    dx, dy = points[-1][0] - x4, points[-1][1] - y4
+    if (dx * dx + dy * dy) > epsilon:
+        points.append((x4, y4))
+
+    return points
+
+
+def arc(cx, cy, rx, ry, a1, a2, ccw=False):
+    scale = 1.0
+    ra = (abs(rx) + abs(ry)) / 2.0
+    da = math.acos(ra / (ra + 0.125 / scale)) * 2.0
+    if ccw:
+        while a2 < a1:
+            a2 += math.pi * 2.0
+    else:
+        while a1 < a2:
+            a1 += math.pi * 2.0
+        da = -da
+    a_start = a1
+    a_end = a2
+
+    vertices = []
+    angle = a_start
+    while (angle < a_end - da / 4) == ccw:
+        x = cx + math.cos(angle) * rx
+        y = cy + math.sin(angle) * ry
+        vertices.append((x, y))
+        angle += da
+    x = cx + math.cos(a_end) * rx
+    y = cy + math.sin(a_end) * ry
+    vertices.append((x, y))
+    return vertices
+
+
+def elliptical_arc(x0, y0, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2):
+    radii_ok = True
+    cos_a = math.cos(angle)
+    sin_a = math.sin(angle)
+    if rx < 0.0:
+        rx = -rx
+    if ry < 0.0:
+        ry = -rx
+
+    # Calculate the middle point between
+    # the current and the final points
+    # ------------------------
+    dx2 = (x0 - x2) / 2.0
+    dy2 = (y0 - y2) / 2.0
+
+    # Calculate (x1, y1)
+    # ------------------------
+    x1 = cos_a * dx2 + sin_a * dy2
+    y1 = -sin_a * dx2 + cos_a * dy2
+
+    # Check that radii are large enough
+    # ------------------------
+    prx, pry = rx * rx, ry * ry
+    px1, py1 = x1 * x1, y1 * y1
+
+    radii_check = px1 / prx + py1 / pry
+    if radii_check > 1.0:
+        rx = math.sqrt(radii_check) * rx
+        ry = math.sqrt(radii_check) * ry
+        prx = rx * rx
+        pry = ry * ry
+        if radii_check > 10.0:
+            radii_ok = False  # noqa
+
+    # Calculate (cx1, cy1)
+    # ------------------------
+    if large_arc_flag == sweep_flag:
+        sign = -1
+    else:
+        sign = +1
+    sq = (prx * pry - prx * py1 - pry * px1) / (prx * py1 + pry * px1)
+    coef = sign * math.sqrt(max(sq, 0))
+    cx1 = coef * ((rx * y1) / ry)
+    cy1 = coef * -((ry * x1) / rx)
+
+    # Calculate (cx, cy) from (cx1, cy1)
+    # ------------------------
+    sx2 = (x0 + x2) / 2.0
+    sy2 = (y0 + y2) / 2.0
+    cx = sx2 + (cos_a * cx1 - sin_a * cy1)
+    cy = sy2 + (sin_a * cx1 + cos_a * cy1)
+
+    # Calculate the start_angle (angle1) and the sweep_angle (dangle)
+    # ------------------------
+    ux = (x1 - cx1) / rx
+    uy = (y1 - cy1) / ry
+    vx = (-x1 - cx1) / rx
+    vy = (-y1 - cy1) / ry
+
+    # Calculate the angle start
+    # ------------------------
+    n = math.sqrt(ux * ux + uy * uy)
+    p = ux
+    if uy < 0:
+        sign = -1.0
+    else:
+        sign = +1.0
+    v = p / n
+    if v < -1.0:
+        v = -1.0
+    if v > 1.0:
+        v = 1.0
+    start_angle = sign * math.acos(v)
+
+    # Calculate the sweep angle
+    # ------------------------
+    n = math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
+    p = ux * vx + uy * vy
+    if ux * vy - uy * vx < 0:
+        sign = -1.0
+    else:
+        sign = +1.0
+    v = p / n
+    v = min(max(v, -1.0), +1.0)
+    sweep_angle = sign * math.acos(v)
+    if not sweep_flag and sweep_angle > 0:
+        sweep_angle -= math.pi * 2.0
+    elif sweep_flag and sweep_angle < 0:
+        sweep_angle += math.pi * 2.0
+
+    start_angle = math.fmod(start_angle, 2.0 * math.pi)
+    if sweep_angle >= 2.0 * math.pi:
+        sweep_angle = 2.0 * math.pi
+    if sweep_angle <= -2.0 * math.pi:
+        sweep_angle = -2.0 * math.pi
+
+    V = arc(cx, cy, rx, ry, start_angle, start_angle + sweep_angle, sweep_flag)
+    c = math.cos(angle)
+    s = math.sin(angle)
+    X, Y = V[:, 0] - cx, V[:, 1] - cy
+    V[:, 0] = c * X - s * Y + cx
+    V[:, 1] = s * X + c * Y + cy
+    return V
diff --git a/vispy/util/svg/group.py b/vispy/util/svg/group.py
new file mode 100644
index 0000000..7adc18e
--- /dev/null
+++ b/vispy/util/svg/group.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+
+import copy
+from vispy.util import logger
+from . path import Path
+from . base import namespace
+from . transformable import Transformable
+
+
+class Group(Transformable):
+
+    def __init__(self, content=None, parent=None):
+        Transformable.__init__(self, content, parent)
+
+        self._items = []
+        for element in content:
+            if not element.tag.startswith(namespace):
+                continue
+            tag = element.tag[len(namespace):]
+            if tag == "g":
+                item = Group(element, self)
+            elif tag == "path":
+                item = Path(element, self)
+            else:
+                logger.warn("Unhandled SVG tag (%s)" % tag)
+                continue
+            self._items.append(item)
+
+    @property
+    def flatten(self):
+        i = 0
+        L = copy.deepcopy(self._items)
+        while i < len(L):
+            while isinstance(L[i], Group) and len(L[i]._items):
+                L[i:i + 1] = L[i]._items
+            i += 1
+        return L
+
+    @property
+    def paths(self):
+        return [item for item in self.flatten if isinstance(item, Path)]
+
+    def __repr__(self):
+        s = ""
+        for item in self._items:
+            s += repr(item)
+        return s
+
+    @property
+    def xml(self):
+        return self._xml()
+
+    def _xml(self, prefix=""):
+        s = prefix + "<g "
+        s += 'id="%s" ' % self._id
+        s += self._transform.xml
+        s += self._style.xml
+        s += ">\n"
+        for item in self._items:
+            s += item._xml(prefix=prefix + "   ")
+        s += prefix + "</g>\n"
+        return s
diff --git a/vispy/util/svg/length.py b/vispy/util/svg/length.py
new file mode 100644
index 0000000..b7d27a0
--- /dev/null
+++ b/vispy/util/svg/length.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+import re
+import math
+
+from . base import units
+from .. import logger
+
+
+class Length(object):
+
+    def __init__(self, content, mode='x', parent=None):
+
+        if not content:
+            self._unit = None
+            self._value = 0
+            self._computed_value = 0
+            return
+
+        re_number = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?'
+        re_unit = r'em|ex|px|in|cm|mm|pt|pc|%'
+        re_length = r'(?P<value>%s)\s*(?P<unit>%s)*' % (re_number, re_unit)
+        match = re.match(re_length, content)
+
+        if match:
+            self._value = float(match.group("value"))
+            self._unit = match.group("unit") or "px"
+        else:
+            self._value = 0.0
+            self._unit = None
+
+        scale = 1
+        if self._unit == '%':
+            if not parent:
+                logger.warn("No parent for computing length using percent")
+            elif hasattr(parent, 'viewport'):
+                w, h = parent.viewport
+                if mode == 'x':
+                    scale = w
+                elif mode == 'y':
+                    scale = h
+                elif mode == 'xy':
+                    scale = math.sqrt(w * w + h * h) / math.sqrt(2.0)
+            else:
+                logger.warn("Parent doesn't have a viewport")
+
+        self._computed_value = self._value * units[self._unit] * scale
+
+    def __float__(self):
+        return self._computed_value
+
+    @property
+    def value(self):
+        return self._computed_value
+
+    def __repr__(self):
+        if self._unit:
+            return "%g%s" % (self._value, self._unit)
+        else:
+            return "%g" % (self._value)
+
+
+class XLength(Length):
+
+    def __init__(self, content, parent=None):
+        Length.__init__(self, content, 'x', parent)
+
+
+class YLength(Length):
+
+    def __init__(self, content, parent=None):
+        Length.__init__(self, content, 'y', parent)
+
+
+class XYLength(Length):
+
+    def __init__(self, content, parent=None):
+        Length.__init__(self, content, 'xy', parent)
diff --git a/vispy/util/svg/number.py b/vispy/util/svg/number.py
new file mode 100644
index 0000000..693af7a
--- /dev/null
+++ b/vispy/util/svg/number.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+
+class Number(object):
+
+    def __init__(self, content):
+        if not content:
+            self._value = 0
+        else:
+            content = content.strip()
+            self._value = float(content)
+
+    def __float__(self):
+        return self._value
+
+    @property
+    def value(self):
+        return self._value
+
+    def __repr__(self):
+        return repr(self._value)
diff --git a/vispy/util/svg/path.py b/vispy/util/svg/path.py
new file mode 100644
index 0000000..e43454a
--- /dev/null
+++ b/vispy/util/svg/path.py
@@ -0,0 +1,331 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+import re
+import math
+import numpy as np
+
+from . import geometry
+from . geometry import epsilon
+from . transformable import Transformable
+
+
+# ----------------------------------------------------------------- Command ---
+class Command(object):
+
+    def __repr__(self):
+        s = '%s ' % self._command
+        for arg in self._args:
+            s += "%.2f " % arg
+        return s
+
+    def origin(self, current=None, previous=None):
+        relative = self._command in "mlvhcsqtaz"
+
+        if relative and current:
+            return current
+        else:
+            return 0.0, 0.0
+
+
+# -------------------------------------------------------------------- Line ---
+class Line(Command):
+
+    def __init__(self, x=0, y=0, relative=True):
+        self._command = 'l' if relative else 'L'
+        self._args = [x, y]
+
+    def vertices(self, current, previous=None):
+        ox, oy = self.origin(current)
+        x, y = self._args
+        self.previous = x, y
+
+        return (ox + x, oy + y),
+
+
+# ------------------------------------------------------------------- VLine ---
+class VLine(Command):
+
+    def __init__(self, y=0, relative=True):
+        self._command = 'v' if relative else 'V'
+        self._args = [y]
+
+    def vertices(self, current, previous=None):
+        ox, oy = self.origin(current)
+        y = self._args[0]
+        self.previous = ox, oy + y
+
+        return (ox, oy + y),
+
+
+# ------------------------------------------------------------------- HLine ---
+class HLine(Command):
+
+    def __init__(self, x=0, relative=True):
+        self._command = 'h' if relative else 'H'
+        self._args = [x]
+
+    def vertices(self, current, previous=None):
+        ox, oy = self.origin(current)
+        x = self._args[0]
+        self.previous = ox + x, oy
+
+        return (ox + x, oy),
+
+
+# -------------------------------------------------------------------- Move ---
+class Move(Command):
+
+    def __init__(self, x=0, y=0, relative=True):
+        self._command = 'm' if relative else 'M'
+        self._args = [x, y]
+
+    def vertices(self, current, previous=None):
+        ox, oy = self.origin(current)
+        x, y = self._args
+        x, y = x + ox, y + oy
+        self.previous = x, y
+        return (x, y),
+
+
+# ------------------------------------------------------------------- Close ---
+class Close(Command):
+
+    def __init__(self, relative=True):
+        self._command = 'z' if relative else 'Z'
+        self._args = []
+
+    def vertices(self, current, previous=None):
+        self.previous = current
+        return []
+
+
+# --------------------------------------------------------------------- Arc ---
+class Arc(Command):
+
+    def __init__(self, r1=1, r2=1, angle=2 * math.pi, large=True, sweep=True,
+                 x=0, y=0, relative=True):
+        self._command = 'a' if relative else 'A'
+        self._args = [r1, r2, angle, large, sweep, x, y]
+
+    def vertices(self, current, previous=None):
+        ox, oy = self.origin(current)
+        rx, ry, angle, large, sweep, x, y = self._args
+        x, y = x + ox, y + oy
+        x0, y0 = current
+        self.previous = x, y
+        vertices = geometry.elliptical_arc(
+            x0, y0, rx, ry, angle, large, sweep, x, y)
+        return vertices[1:]
+
+
+# ------------------------------------------------------------------- Cubic ---
+class Cubic(Command):
+
+    def __init__(self, x1=0, y1=0, x2=0, y2=0, x3=0, y3=0, relative=True):
+        self._command = 'c' if relative else 'C'
+        self._args = [x1, y1, x2, y2, x3, y3]
+
+    def vertices(self, current, previous=None):
+        ox, oy = self.origin(current)
+        x0, y0 = current
+        x1, y1, x2, y2, x3, y3 = self._args
+        x1, y1 = x1 + ox, y1 + oy
+        x2, y2 = x2 + ox, y2 + oy
+        x3, y3 = x3 + ox, y3 + oy
+        self.previous = x2, y2
+        vertices = geometry.cubic((x0, y0), (x1, y1), (x2, y2), (x3, y3))
+        return vertices[1:]
+
+
+# --------------------------------------------------------------- Quadratic ---
+class Quadratic(Command):
+
+    def __init__(self, x1=0, y1=0, x2=0, y2=0, relative=True):
+        self._command = 'q' if relative else 'Q'
+        self._args = [x1, y1, x2, y2]
+
+    def vertices(self, current, last_control_point=None):
+        ox, oy = self.origin(current)
+        x1, y1, x2, y2 = self._args
+        x0, y0 = current
+        x1, y1 = x1 + ox, y1 + oy
+        x2, y2 = x2 + ox, y2 + oy
+        self.previous = x1, y1
+        vertices = geometry.quadratic((x0, y0), (x1, y1), (x2, y2))
+
+        return vertices[1:]
+
+
+# ------------------------------------------------------------- SmoothCubic ---
+class SmoothCubic(Command):
+
+    def __init__(self, x2=0, y2=0, x3=0, y3=0, relative=True):
+        self._command = 's' if relative else 'S'
+        self._args = [x2, y2, x3, y3]
+
+    def vertices(self, current, previous):
+        ox, oy = self.origin(current)
+        x0, y0 = current
+        x2, y2, x3, y3 = self._args
+        x2, y2 = x2 + ox, y2 + oy
+        x3, y3 = x3 + ox, y3 + oy
+        x1, y1 = 2 * x0 - previous[0], 2 * y0 - previous[1]
+        self.previous = x2, y2
+        vertices = geometry.cubic((x0, y0), (x1, y1), (x2, y2), (x3, y3))
+
+        return vertices[1:]
+
+
+# --------------------------------------------------------- SmoothQuadratic ---
+class SmoothQuadratic(Command):
+
+    def __init__(self, x2=0, y2=0, relative=True):
+        self._command = 't' if relative else 'T'
+        self._args = [x2, y2]
+
+    def vertices(self, current, previous):
+        ox, oy = self.origin(current)
+        x2, y2 = self._args
+        x0, y0 = current
+        x1, y1 = 2 * x0 - previous[0], 2 * y0 - previous[1]
+        x2, y2 = x2 + ox, y2 + oy
+        self.previous = x1, y1
+        vertices = geometry.quadratic((x0, y0), (x1, y1), (x2, y2))
+
+        return vertices[1:]
+
+
+# -------------------------------------------------------------------- Path ---
+class Path(Transformable):
+
+    def __init__(self, content=None, parent=None):
+        Transformable.__init__(self, content, parent)
+        self._paths = []
+
+        if not isinstance(content, str):
+            content = content.get("d", "")
+
+        commands = re.compile(
+            "(?P<command>[MLVHCSQTAZmlvhcsqtaz])(?P<points>[+\-0-9.e, \n\t]*)")
+
+        path = []
+        for match in re.finditer(commands, content):
+            command = match.group("command")
+            points = match.group("points").replace(',', ' ')
+            points = [float(v) for v in points.split()]
+            relative = command in "mlvhcsqtaz"
+            command = command.upper()
+
+            while len(points) or command == 'Z':
+                if command == 'M':
+                    if len(path):
+                        self._paths.append(path)
+                    path = []
+                    path.append(Move(*points[:2], relative=relative))
+                    points = points[2:]
+                elif command == 'L':
+                    path.append(Line(*points[:2], relative=relative))
+                    points = points[2:]
+                elif command == 'V':
+                    path.append(VLine(*points[:1], relative=relative))
+                    points = points[1:]
+                elif command == 'H':
+                    path.append(HLine(*points[:1], relative=relative))
+                    points = points[1:]
+                elif command == 'C':
+                    path.append(Cubic(*points[:6], relative=relative))
+                    points = points[6:]
+                elif command == 'S':
+                    path.append(SmoothCubic(*points[:4], relative=relative))
+                    points = points[4:]
+                elif command == 'Q':
+                    path.append(Quadratic(*points[:4], relative=relative))
+                    points = points[4:]
+                elif command == 'T':
+                    path.append(
+                        SmoothQuadratic(*points[2:], relative=relative))
+                    points = points[2:]
+                elif command == 'A':
+                    path.append(Arc(*points[:7], relative=relative))
+                    points = points[7:]
+                elif command == 'Z':
+                    path.append(Close(relative=relative))
+                    self._paths.append(path)
+                    path = []
+                    break
+                else:
+                    raise RuntimeError(
+                        "Unknown SVG path command(%s)" % command)
+
+        if len(path):
+            self._paths.append(path)
+
+    def __repr__(self):
+        s = ""
+        for path in self._paths:
+            for item in path:
+                s += repr(item)
+        return s
+
+    @property
+    def xml(self):
+        return self._xml()
+
+    def _xml(self, prefix=""):
+        s = prefix + "<path "
+        s += 'id="%s" ' % self._id
+        s += self._style.xml
+        s += '\n'
+        t = '     ' + prefix + ' d="'
+        s += t
+        prefix = ' ' * len(t)
+        first = True
+        for i, path in enumerate(self._paths):
+            for j, item in enumerate(path):
+                if first:
+                    s += repr(item)
+                    first = False
+                else:
+                    s += prefix + repr(item)
+                if i < len(self._paths) - 1 or j < len(path) - 1:
+                    s += '\n'
+        s += '"/>\n'
+        return s
+
+    @property
+    def vertices(self):
+        self._vertices = []
+        current = 0, 0
+        previous = 0, 0
+
+        for path in self._paths:
+            vertices = []
+            for command in path:
+                V = command.vertices(current, previous)
+                previous = command.previous
+                vertices.extend(V)
+                if len(V) > 0:
+                    current = V[-1]
+                else:
+                    current = 0, 0
+
+            closed = False
+            if isinstance(command, Close):
+                closed = True
+                if len(vertices) > 2:
+                    d = geometry.calc_sq_distance(vertices[-1][0], vertices[-1][1],  # noqa
+                                                  vertices[0][0],  vertices[0][1])  # noqa
+                    if d < epsilon:
+                        vertices = vertices[:-1]
+
+            # Apply transformation
+            V = np.ones((len(vertices), 3))
+            V[:, :2] = vertices
+            V = np.dot(V, self.transform.matrix.T)
+            V[:, 2] = 0
+            self._vertices.append((V, closed))
+
+        return self._vertices
diff --git a/vispy/util/svg/shapes.py b/vispy/util/svg/shapes.py
new file mode 100644
index 0000000..e061cb3
--- /dev/null
+++ b/vispy/util/svg/shapes.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+
+class Rect(object):
+
+    def __init__(self, x=0, y=0, width=1, height=1, rx=0, ry=0):
+        self.x = x
+        self.y = y
+        self.width = width
+        self.height = height
+        self.rx = rx
+        self.ry = ry
+
+    def parse(self, expression):
+        """ """
+
+
+class Line(object):
+
+    def __init__(self, x1=0, y1=0, x2=0, y2=0):
+        self.x1 = x2
+        self.y1 = y2
+        self.x2 = x2
+        self.y2 = y2
+
+
+class Circle(object):
+
+    def __init__(self, cx=0, cy=0, r=1):
+        self.cx = cx
+        self.cy = cy
+        self.r = r
+
+
+class Ellipse(object):
+
+    def __init__(self, cx=0, cy=0, rx=1, ry=1):
+        self.cx = cx
+        self.cy = cy
+        self.rx = rx
+        self.ry = ry
+
+
+class Polygon(object):
+
+    def __init__(self, points=[]):
+        self.points = points
+
+
+class Polyline(object):
+
+    def __init__(self, points=[]):
+        self.points = points
diff --git a/vispy/util/svg/style.py b/vispy/util/svg/style.py
new file mode 100644
index 0000000..9112c03
--- /dev/null
+++ b/vispy/util/svg/style.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+from . color import Color
+from . number import Number
+from . length import Length
+
+_converters = {
+    "fill":              Color,
+    "fill-opacity":      Number,
+    "stroke":            Color,
+    "stroke-opacity":    Number,
+    "opacity":           Number,
+    "stroke-width":      Length,
+    #    "stroke-miterlimit": Number,
+    #    "stroke-dasharray":  Lengths,
+    #    "stroke-dashoffset": Length,
+}
+
+
+class Style(object):
+
+    def __init__(self):
+        self._unset = True
+        for key in _converters.keys():
+            key_ = key.replace("-", "_")
+            self.__setattr__(key_, None)
+
+    def update(self, content):
+        if not content:
+            return
+
+        self._unset = False
+        items = content.strip().split(";")
+        attributes = dict([item.strip().split(":") for item in items if item])
+        for key, value in attributes.items():
+            if key in _converters:
+                key_ = key.replace("-", "_")
+                self.__setattr__(key_, _converters[key](value))
+
+    @property
+    def xml(self):
+        return self._xml()
+
+    def _xml(self, prefix=""):
+        if self._unset:
+            return ""
+
+        s = 'style="'
+        for key in _converters.keys():
+            key_ = key.replace("-", "_")
+            value = self.__getattribute__(key_)
+            if value is not None:
+                s += '%s:%s ' % (key, value)
+        s += '"'
+        return s
diff --git a/vispy/util/svg/svg.py b/vispy/util/svg/svg.py
new file mode 100644
index 0000000..738d718
--- /dev/null
+++ b/vispy/util/svg/svg.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+
+from . group import Group
+from . viewport import Viewport
+
+
+class SVG(Group):
+
+    def __init__(self, content=None, parent=None):
+        Group.__init__(self, content, parent)
+        self._viewport = Viewport(content)
+
+    @property
+    def viewport(self):
+        return self._viewport
+
+    def __repr__(self):
+        s = ""
+        for item in self._items:
+            s += repr(item) + "\n"
+        return s
+
+    @property
+    def xml(self):
+        return self._xml()
+
+    def _xml(self, prefix=""):
+        s = "<svg "
+        s += 'id="%s" ' % self._id
+        s += self._viewport.xml
+        s += self._transform.xml
+        s += "\n"
+        for item in self._items:
+            s += item._xml(prefix=prefix + "    ") + "\n"
+        s += "</svg>\n"
+        return s
diff --git a/vispy/util/svg/transform.py b/vispy/util/svg/transform.py
new file mode 100644
index 0000000..2f2c593
--- /dev/null
+++ b/vispy/util/svg/transform.py
@@ -0,0 +1,229 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+import re
+import math
+import numpy as np
+
+# ------------------------------------------------------------------ Matrix ---
+
+
+class Matrix(object):
+
+    def __init__(self, a=1, b=0, c=0, d=1, e=0, f=0):
+        self._matrix = np.array([[a, c, e],
+                                 [b, d, f],
+                                 [0, 0, 1]], dtype=float)
+
+    @property
+    def matrix(self):
+        return self._matrix
+
+    def __array__(self, *args):
+        return self._matrix
+
+    def __repr__(self):
+        a, c, e = self._matrix[0]
+        b, d, f = self._matrix[1]
+        return "Matrix(%g,%g,%g,%g,%g,%g)" % (a, b, c, d, e, f)
+
+
+# ---------------------------------------------------------------- Identity ---
+class Identity(Matrix):
+
+    def __init__(self):
+        Matrix.__init__(self)
+        self._matrix[...] = ([[1, 0, 0],
+                              [0, 1, 0],
+                              [0, 0, 1]])
+
+    def __repr__(self):
+        return "Identity()"
+
+
+# --------------------------------------------------------------- Translate ---
+class Translate(Matrix):
+
+    """
+    Translation is equivalent to the matrix [1 0 0 1 tx ty], where tx and ty
+    are the distances to translate coordinates in X and Y, respectively.
+    """
+
+    def __init__(self, x, y=0):
+        Matrix.__init__(self)
+        self._x, self._y = x, y
+        self._matrix[...] = ([[1, 0, x],
+                              [0, 1, y],
+                              [0, 0, 1]])
+
+    def __repr__(self):
+        return "Translate(%g,%g)" % (self._x, self._y)
+
+
+# ------------------------------------------------------------------- Scale ---
+class Scale(Matrix):
+
+    """
+    Scaling is equivalent to the matrix [sx 0 0 sy 0 0]. One unit in the X and
+    Y directions in the new coordinate system equals sx and sy units in the
+    previous coordinate system, respectively.
+    """
+
+    def __init__(self, x, y=0):
+        Matrix.__init__(self)
+        self._x = x
+        self._y = y or x
+        self._matrix[...] = ([[x, 0, 0],
+                              [0, y, 0],
+                              [0, 0, 1]])
+
+    def __repr__(self):
+        return "Scale(%g,%g)" % (self._x, self._y)
+
+
+# ------------------------------------------------------------------- Scale ---
+class Rotate(Matrix):
+
+    """
+    Rotation about the origin is equivalent to the matrix [cos(a) sin(a)
+    -sin(a) cos(a) 0 0], which has the effect of rotating the coordinate system
+    axes by angle a.
+    """
+
+    def __init__(self, angle, x=0, y=0):
+        Matrix.__init__(self)
+        self._angle = angle
+        self._x = x
+        self._y = y
+
+        angle = math.pi * angle / 180.0
+        rotate = np.array([[math.cos(angle), -math.sin(angle), 0],
+                           [math.sin(angle),  math.cos(angle), 0],
+                           [0, 0, 1]], dtype=float)
+        forward = np.array([[1, 0, x],
+                            [0, 1, y],
+                            [0, 0, 1]], dtype=float)
+        inverse = np.array([[1, 0, -x],
+                            [0, 1, -y],
+                            [0, 0, 1]], dtype=float)
+        self._matrix = np.dot(inverse, np.dot(rotate, forward))
+
+    def __repr__(self):
+        return "Rotate(%g,%g,%g)" % (self._angle, self._x, self._y)
+
+
+# ------------------------------------------------------------------- SkewX ---
+class SkewX(Matrix):
+
+    """
+    A skew transformation along the x-axis is equivalent to the matrix [1 0
+    tan(a) 1 0 0], which has the effect of skewing X coordinates by angle a.
+    """
+
+    def __init__(self, angle):
+        Matrix.__init__(self)
+        self._angle = angle
+        angle = math.pi * angle / 180.0
+        self._matrix[...] = ([[1, math.tan(angle), 0],
+                              [0, 1, 0],
+                              [0, 0, 1]])
+
+    def __repr__(self):
+        return "SkewX(%g)" % (self._angle)
+
+
+# ------------------------------------------------------------------- SkewY ---
+class SkewY(Matrix):
+
+    """
+    A skew transformation along the y-axis is equivalent to the matrix [1
+    tan(a) 0 1 0 0], which has the effect of skewing Y coordinates by angle a.
+    """
+
+    def __init__(self, angle):
+        Matrix.__init__(self)
+        self._angle = angle
+        angle = math.pi * angle / 180.0
+        self._matrix[...] = ([[1, 0, 0],
+                              [math.tan(angle), 1, 0],
+                              [0, 0, 1]])
+
+    def __repr__(self):
+        return "SkewY(%g)" % (self._angle)
+
+
+# --------------------------------------------------------------- Transform ---
+class Transform:
+
+    """
+    A Transform is defined as a list of transform definitions, which are
+    applied in the order provided. The individual transform definitions are
+    separated by whitespace and/or a comma.
+    """
+
+    def __init__(self, content=""):
+        self._transforms = []
+        if not content:
+            return
+
+        converters = {"matrix":    Matrix,
+                      "scale":     Scale,
+                      "rotate":    Rotate,
+                      "translate": Translate,
+                      "skewx":     SkewX,
+                      "skewy":     SkewY}
+        keys = "|".join(converters.keys())
+        pattern = "(?P<name>%s)\s*\((?P<args>[^)]*)\)" % keys
+
+        for match in re.finditer(pattern, content):
+            name = match.group("name").strip()
+            args = match.group("args").strip().replace(',', ' ')
+            args = [float(value) for value in args.split()]
+            transform = converters[name](*args)
+            self._transforms.append(transform)
+
+    def __add__(self, other):
+        T = Transform()
+        T._transforms.extend(self._transforms)
+        T._transforms.extend(other._transforms)
+        return T
+
+    def __radd__(self, other):
+        self._transforms.extend(other._transforms)
+        return self
+
+    @property
+    def matrix(self):
+        M = np.eye(3)
+        for transform in self._transforms:
+            M = np.dot(M, transform)
+        return M
+
+    def __array__(self, *args):
+        return self._matrix
+
+    def __repr__(self):
+        s = ""
+        for i in range(len(self._transforms)):
+            s += repr(self._transforms[i])
+            if i < len(self._transforms) - 1:
+                s += ", "
+        return s
+
+    @property
+    def xml(self):
+        return self._xml()
+
+    def _xml(self, prefix=""):
+
+        identity = True
+        for transform in self._transforms:
+            if not isinstance(transform, Identity):
+                identity = False
+                break
+        if identity:
+            return ""
+
+        return 'transform="%s" ' % repr(self)
diff --git a/vispy/util/svg/transformable.py b/vispy/util/svg/transformable.py
new file mode 100644
index 0000000..28e006b
--- /dev/null
+++ b/vispy/util/svg/transformable.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+from . element import Element
+from . transform import Transform
+
+
+class Transformable(Element):
+
+    """ Transformable SVG element """
+
+    def __init__(self, content=None, parent=None):
+        Element.__init__(self, content, parent)
+
+        if isinstance(content, str):
+            self._transform = Transform()
+            self._computed_transform = self._transform
+        else:
+            self._transform = Transform(content.get("transform", None))
+            self._computed_transform = self._transform
+            if parent:
+                self._computed_transform = self._transform + \
+                    self.parent.transform
+
+    @property
+    def transform(self):
+        return self._computed_transform
diff --git a/vispy/util/svg/viewport.py b/vispy/util/svg/viewport.py
new file mode 100644
index 0000000..7874a05
--- /dev/null
+++ b/vispy/util/svg/viewport.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+
+from . length import XLength, YLength
+
+
+class Viewport(object):
+
+    def __init__(self, content=None, parent=None):
+
+        self._x = None
+        self._computed_x = 0
+        if content.get('x'):
+            self._x = XLength(content.get('x'), parent)
+            self._computed_x = float(self._x)
+
+        self._y = None
+        self._computed_y = 0
+        if content.get('y'):
+            self._y = XLength(content.get('y'), parent)
+            self._computed_y = float(self._y)
+
+        self._width = None
+        self._computed_width = 800
+        if content.get('width'):
+            self._width = XLength(content.get('width'), parent)
+            self._computed_width = float(self._width)
+
+        self._height = None
+        self._computed_height = 800
+        if content.get('height'):
+            self._height = YLength(content.get('height'), parent)
+            self._computed_height = float(self._height)
+
+    @property
+    def x(self):
+        return self._computed_x
+
+    @property
+    def y(self):
+        return self._computed_y
+
+    @property
+    def width(self):
+        return self._computed_width
+
+    @property
+    def height(self):
+        return self._computed_height
+
+    def __repr__(self):
+        s = repr((self._x, self._y, self._width, self._height))
+        return s
+
+    @property
+    def xml(self):
+        return self._xml
+
+    @property
+    def _xml(self, prefix=""):
+        s = ""
+        if self._x:
+            s += 'x="%s" ' % repr(self._x)
+        if self._y:
+            s += 'y="%s" ' % repr(self._y)
+        if self._width:
+            s += 'width="%s" ' % repr(self._width)
+        if self._height:
+            s += 'height="%s" ' % repr(self._height)
+        return s
diff --git a/vispy/util/tests/test_config.py b/vispy/util/tests/test_config.py
index 0036867..be5ff21 100644
--- a/vispy/util/tests/test_config.py
+++ b/vispy/util/tests/test_config.py
@@ -1,10 +1,13 @@
-from nose.tools import assert_raises, assert_equal, assert_true
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 from os import path as op
 import os
 
-from vispy.util import config, sys_info, _TempDir, set_data_dir, save_config
-from vispy.util.fetching import load_data_file
-from vispy.testing import assert_in, requires_application
+from vispy.util import (config, sys_info, _TempDir, set_data_dir, save_config,
+                        load_data_file)
+from vispy.testing import (assert_in, requires_application, run_tests_if_main,
+                           assert_raises, assert_equal, assert_true)
 temp_dir = _TempDir()
 
 
@@ -16,8 +19,7 @@ def test_sys_info():
     assert_raises(IOError, sys_info, fname)  # no overwrite
     with open(fname, 'r') as fid:
         out = ''.join(fid.readlines())
-    # Note: 'GL version' only for non-GLUT
-    keys = ['Python', 'Backend', 'pyglet', 'Platform:']
+    keys = ['GL version', 'Python', 'Backend', 'pyglet', 'Platform:']
     for key in keys:
         assert_in(key, out)
     print(out)
@@ -49,3 +51,6 @@ def test_config():
             os.environ['_VISPY_CONFIG_TESTING'] = orig_val
         else:
             del os.environ['_VISPY_CONFIG_TESTING']
+
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_docstring_parameters.py b/vispy/util/tests/test_docstring_parameters.py
new file mode 100644
index 0000000..bc3c732
--- /dev/null
+++ b/vispy/util/tests/test_docstring_parameters.py
@@ -0,0 +1,123 @@
+# TODO inspect for Cython (see sagenb.misc.sageinspect)
+from __future__ import print_function
+
+from nose.plugins.skip import SkipTest
+from os import path as op
+import inspect
+import warnings
+import imp
+from vispy.testing import run_tests_if_main
+
+public_modules = [
+    # the list of modules users need to access for all functionality
+    'vispy',
+    'vispy.color',
+    'vispy.geometry',
+    'vispy.gloo',
+    'vispy.io',
+    'vispy.mpl_plot',
+    'vispy.plot',
+    'vispy.scene',
+    'vispy.util',
+    'vispy.visuals',
+]
+
+docscrape_path = op.join(op.dirname(__file__), '..', '..', '..', 'doc', 'ext',
+                         'docscrape.py')
+if op.isfile(docscrape_path):
+    docscrape = imp.load_source('docscrape', docscrape_path)
+else:
+    docscrape = None
+
+
+def get_name(func):
+    parts = []
+    module = inspect.getmodule(func)
+    if module:
+        parts.append(module.__name__)
+    if hasattr(func, 'im_class'):
+        parts.append(func.im_class.__name__)
+    parts.append(func.__name__)
+    return '.'.join(parts)
+
+
+# functions to ignore
+_ignores = [
+    'vispy.scene.visuals',  # not parsed properly by this func, copies anyway
+]
+
+
+def check_parameters_match(func, doc=None):
+    """Helper to check docstring, returns list of incorrect results"""
+    incorrect = []
+    name_ = get_name(func)
+    if not name_.startswith('vispy.'):
+        return incorrect
+    if inspect.isdatadescriptor(func):
+        return incorrect
+    args, varargs, varkw, defaults = inspect.getargspec(func)
+    # drop self
+    if len(args) > 0 and args[0] in ('self', 'cls'):
+        args = args[1:]
+
+    if doc is None:
+        with warnings.catch_warnings(record=True) as w:
+            doc = docscrape.FunctionDoc(func)
+        if len(w):
+            raise RuntimeError('Error for %s:\n%s' % (name_, w[0]))
+    # check set
+    param_names = [name for name, _, _ in doc['Parameters']]
+    # clean up some docscrape output:
+    param_names = [name.split(':')[0].strip('` ') for name in param_names]
+    param_names = [name for name in param_names if '*' not in name]
+    if len(param_names) != len(args):
+        bad = str(sorted(list(set(param_names) - set(args)) +
+                         list(set(args) - set(param_names))))
+        if not any(d in name_ for d in _ignores):
+            incorrect += [name_ + ' arg mismatch: ' + bad]
+    else:
+        for n1, n2 in zip(param_names, args):
+            if n1 != n2:
+                incorrect += [name_ + ' ' + n1 + ' != ' + n2]
+    return incorrect
+
+
+def test_docstring_parameters():
+    """Test module docsting formatting"""
+    if docscrape is None:
+        raise SkipTest('This must be run from the vispy source directory')
+    incorrect = []
+    for name in public_modules:
+        module = __import__(name, globals())
+        for submod in name.split('.')[1:]:
+            module = getattr(module, submod)
+        classes = inspect.getmembers(module, inspect.isclass)
+        for cname, cls in classes:
+            if cname.startswith('_'):
+                continue
+            with warnings.catch_warnings(record=True) as w:
+                cdoc = docscrape.ClassDoc(cls)
+            if len(w):
+                raise RuntimeError('Error for __init__ of %s in %s:\n%s'
+                                   % (cls, name, w[0]))
+            if hasattr(cls, '__init__'):
+                incorrect += check_parameters_match(cls.__init__, cdoc)
+            for method_name in cdoc.methods:
+                method = getattr(cls, method_name)
+                # skip classes that are added as attributes of classes
+                if (inspect.ismethod(method) or inspect.isfunction(method)):
+                    incorrect += check_parameters_match(method)
+            if hasattr(cls, '__call__'):
+                incorrect += check_parameters_match(cls.__call__)
+        functions = inspect.getmembers(module, inspect.isfunction)
+        for fname, func in functions:
+            if fname.startswith('_'):
+                continue
+            incorrect += check_parameters_match(func)
+    msg = '\n' + '\n'.join(sorted(list(set(incorrect))))
+    if len(incorrect) > 0:
+        msg += '\n\n%s docstring violations found' % msg.count('\n')
+        raise AssertionError(msg)
+
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_emitter_group.py b/vispy/util/tests/test_emitter_group.py
index 8e98bfb..a6a4d23 100644
--- a/vispy/util/tests/test_emitter_group.py
+++ b/vispy/util/tests/test_emitter_group.py
@@ -1,7 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import unittest
 import copy
 
 from vispy.util.event import Event, EventEmitter, EmitterGroup
+from vispy.util import use_log_level
+from vispy.testing import run_tests_if_main, assert_true, assert_raises
 
 
 class BasicEvent(Event):
@@ -10,9 +15,9 @@ class BasicEvent(Event):
 
 class TypedEvent(Event):
 
-    def __init__(self, **kwds):
-        kwds['type'] = 'typed_event'
-        Event.__init__(self, **kwds)
+    def __init__(self, **kwargs):
+        kwargs['type'] = 'typed_event'
+        Event.__init__(self, **kwargs)
 
 
 class TestGroups(unittest.TestCase):
@@ -91,6 +96,20 @@ class TestGroups(unittest.TestCase):
             grp.unblock_all()
         assert self.result is None
 
+    def test_group_ignore(self):
+        """EmitterGroup.block_all"""
+        grp = EmitterGroup(em1=Event)
+        grp.em1.connect(self.error_event)
+        with use_log_level('warning', record=True, print_msg=False) as l:
+            grp.em1()
+        assert_true(len(l) >= 1)
+        grp.ignore_callback_errors = False
+        assert_raises(RuntimeError, grp.em1)
+        grp.ignore_callback_errors = True
+        with use_log_level('warning', record=True, print_msg=False) as l:
+            grp.em1()
+        assert_true(len(l) >= 1)
+
     def test_group_disconnect(self):
         """EmitterGroup.disconnect"""
         grp = EmitterGroup(em1=Event)
@@ -141,8 +160,8 @@ class TestGroups(unittest.TestCase):
     def test_add_custom_emitter(self):
         class Emitter(EventEmitter):
 
-            def _prepare_event(self, *args, **kwds):
-                ev = super(Emitter, self)._prepare_event(*args, **kwds)
+            def _prepare_event(self, *args, **kwargs):
+                ev = super(Emitter, self)._prepare_event(*args, **kwargs)
                 ev.test_key = 1
                 return ev
 
@@ -210,7 +229,10 @@ class TestGroups(unittest.TestCase):
                 self.result = {}
             self.result[key] = ev, attrs
 
-    def assert_result(self, key=None, **kwds):
+    def error_event(self, ev, key=None):
+        raise RuntimeError('Errored')
+
+    def assert_result(self, key=None, **kwargs):
         assert (hasattr(self, 'result') and self.result is not None), \
             "No event recorded"
 
@@ -221,7 +243,7 @@ class TestGroups(unittest.TestCase):
 
         assert isinstance(event, Event), "Emitted object is not Event instance"
 
-        for name, val in kwds.items():
+        for name, val in kwargs.items():
             if name == 'event':
                 assert event is val, "Event objects do not match"
 
@@ -233,3 +255,6 @@ class TestGroups(unittest.TestCase):
                 attr = event_attrs[name]
                 assert (attr == val), "Event.%s != %s  (%s)" % (
                     name, str(val), str(attr))
+
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_event_emitter.py b/vispy/util/tests/test_event_emitter.py
index 1b25664..52f2b71 100644
--- a/vispy/util/tests/test_event_emitter.py
+++ b/vispy/util/tests/test_event_emitter.py
@@ -1,9 +1,12 @@
-from nose.tools import assert_raises, assert_equal
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import unittest
 import copy
 import functools
 
 from vispy.util.event import Event, EventEmitter
+from vispy.testing import run_tests_if_main, assert_raises, assert_equal
 
 
 class BasicEvent(Event):
@@ -12,9 +15,9 @@ class BasicEvent(Event):
 
 class TypedEvent(Event):
 
-    def __init__(self, **kwds):
-        kwds['type'] = 'typed_event'
-        Event.__init__(self, **kwds)
+    def __init__(self, **kwargs):
+        kwargs['type'] = 'typed_event'
+        Event.__init__(self, **kwargs)
 
 
 class TestEmitters(unittest.TestCase):
@@ -92,7 +95,7 @@ class TestEmitters(unittest.TestCase):
         # specifying non-event class should fail (eventually):
         class X:
 
-            def __init__(self, *args, **kwds):
+            def __init__(self, *args, **kwargs):
                 self.blocked = False
 
             def _push_source(self, s):
@@ -135,8 +138,8 @@ class TestEmitters(unittest.TestCase):
         """EventEmitter subclassing"""
         class MyEmitter(EventEmitter):
 
-            def _prepare_event(self, *args, **kwds):
-                ev = super(MyEmitter, self)._prepare_event(*args, **kwds)
+            def _prepare_event(self, *args, **kwargs):
+                ev = super(MyEmitter, self)._prepare_event(*args, **kwargs)
                 ev.test_tag = 1
                 return ev
         em = MyEmitter(type='test_event')
@@ -415,10 +418,10 @@ class TestEmitters(unittest.TestCase):
         em()
         assert self.result == 2
 
-    def try_emitter(self, em, **kwds):
+    def try_emitter(self, em, **kwargs):
         em.connect(self.record_event)
         self.result = None
-        return em(**kwds)
+        return em(**kwargs)
 
     def record_event(self, ev, key=None):
         # get a copy of all event attributes because these may change
@@ -447,7 +450,7 @@ class TestEmitters(unittest.TestCase):
                 self.result = {}
             self.result[key] = ev, attrs
 
-    def assert_result(self, key=None, **kwds):
+    def assert_result(self, key=None, **kwargs):
         assert (hasattr(self, 'result') and self.result is not None), \
             "No event recorded"
 
@@ -458,7 +461,7 @@ class TestEmitters(unittest.TestCase):
 
         assert isinstance(event, Event), "Emitted object is not Event instance"
 
-        for name, val in kwds.items():
+        for name, val in kwargs.items():
             if name == 'event':
                 assert event is val, "Event objects do not match"
 
@@ -658,3 +661,6 @@ def test_emitter_block():
 
     e()
     assert_state(True, True)
+
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_fourier.py b/vispy/util/tests/test_fourier.py
new file mode 100644
index 0000000..ec38774
--- /dev/null
+++ b/vispy/util/tests/test_fourier.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+import numpy as np
+
+from vispy.util.fourier import stft, fft_freqs
+from vispy.testing import assert_raises, run_tests_if_main
+
+
+def test_stft():
+    """Test STFT calculation"""
+    assert_raises(ValueError, stft, 0)
+    assert_raises(ValueError, stft, [], window='foo')
+    assert_raises(ValueError, stft, [[]])
+    result = stft([])
+    assert np.allclose(result, np.zeros_like(result))
+    n_fft = 256
+    step = 128
+    for n_samples, n_estimates in ((256, 1),
+                                   (383, 1), (384, 2),
+                                   (511, 2), (512, 3)):
+        result = stft(np.ones(n_samples), n_fft=n_fft, step=step, window=None)
+        assert result.shape[1] == n_estimates
+        expected = np.zeros(n_fft // 2 + 1)
+        expected[0] = 1
+        for res in result.T:
+            assert np.allclose(expected, np.abs(res))
+            assert np.allclose(expected, np.abs(res))
+    for n_pts, last_freq in zip((256, 255), (500., 498.)):
+        freqs = fft_freqs(n_pts, 1000)
+        assert freqs[0] == 0
+        assert np.allclose(freqs[-1], last_freq, atol=1e-1)
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_import.py b/vispy/util/tests/test_import.py
index 0b3c69a..4b7af88 100644
--- a/vispy/util/tests/test_import.py
+++ b/vispy/util/tests/test_import.py
@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 Test that importing vispy subpackages do not pull
 in any more vispy submodules than strictly necessary.
@@ -5,35 +8,15 @@ in any more vispy submodules than strictly necessary.
 
 import sys
 import os
-import subprocess
-
-from nose.tools import assert_equal
-from vispy.testing import assert_in, assert_not_in, requires_pyopengl
 
+from vispy.testing import (assert_in, assert_not_in, requires_pyopengl,
+                           run_tests_if_main, assert_equal)
+from vispy.util import run_subprocess
 import vispy
 
 
 # minimum that will be imported when importing vispy
-_min_modules = ['vispy', 'vispy.util', 'vispy.ext']
-
-
-def check_output(*popenargs, **kwargs):
-    """ Minimal py 2.6 compatible version of subprocess.check_output()
-
-    Py2.6 does not have check_output.
-    Taken from https://gist.github.com/edufelipe/1027906
-    """
-    process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
-    output, unused_err = process.communicate()
-    retcode = process.poll()
-    if retcode:
-        cmd = kwargs.get("args")
-        if cmd is None:
-            cmd = popenargs[0]
-        error = subprocess.CalledProcessError(retcode, cmd)
-        error.output = output
-        raise error
-    return output
+_min_modules = ['vispy', 'vispy.util', 'vispy.ext', 'vispy.testing']
 
 
 def loaded_vispy_modules(import_module, depth=None, all_modules=False):
@@ -49,13 +32,12 @@ def loaded_vispy_modules(import_module, depth=None, all_modules=False):
 
     # Get the loaded modules in a clean interpreter
     code = "import sys, %s; print(', '.join(sys.modules))" % import_module
-    res = check_output([sys.executable, '-c', code], cwd=vispy_dir)
-    res = res.decode('utf-8')
+    res = run_subprocess([sys.executable, '-c', code], cwd=vispy_dir)[0]
     loaded_modules = [name.strip() for name in res.split(',')]
-    
+
     if all_modules:
         return loaded_modules
-    
+
     # Get only vispy modules at the given depth
     vispy_modules = set()
     for m in loaded_modules:
@@ -88,8 +70,10 @@ def test_import_vispy_util():
 
 def test_import_vispy_app1():
     """ Importing vispy.app should not pull in other vispy submodules. """
+    # Since the introduction of the GLContext to gloo, app depends on gloo
     modnames = loaded_vispy_modules('vispy.app', 2)
-    assert_equal(modnames, set(_min_modules + ['vispy.app']))
+    assert_equal(modnames, set(_min_modules + ['vispy.app', 'vispy.gloo',
+                                               'vispy.glsl', 'vispy.color']))
 
 
 def test_import_vispy_app2():
@@ -98,19 +82,20 @@ def test_import_vispy_app2():
     assert_not_in('PySide', allmodnames)
     assert_not_in('PyQt4', allmodnames)
     assert_not_in('pyglet', allmodnames)
-    assert_not_in('OpenGL.GLUT', allmodnames)
 
 
 def test_import_vispy_gloo():
     """ Importing vispy.gloo should not pull in other vispy submodules. """
     modnames = loaded_vispy_modules('vispy.gloo', 2)
-    assert_equal(modnames, set(_min_modules + ['vispy.gloo', 'vispy.color']))
+    assert_equal(modnames, set(_min_modules + ['vispy.gloo',
+                                               'vispy.glsl',
+                                               'vispy.color']))
 
 
 def test_import_vispy_no_pyopengl():
-    """ Importing vispy.gloo.gl.desktop should not import PyOpenGL. """
+    """ Importing vispy.gloo.gl.gl2 should not import PyOpenGL. """
     # vispy.gloo desktop backend
-    allmodnames = loaded_vispy_modules('vispy.gloo.gl.desktop', 2, True)
+    allmodnames = loaded_vispy_modules('vispy.gloo.gl.gl2', 2, True)
     assert_not_in('OpenGL', allmodnames)
     # vispy.app 
     allmodnames = loaded_vispy_modules('vispy.app', 2, True)
@@ -122,14 +107,18 @@ def test_import_vispy_no_pyopengl():
 
 @requires_pyopengl()
 def test_import_vispy_pyopengl():
-    """ Importing vispy.gloo.gl.pyopengl should import PyOpenGL. """
-    allmodnames = loaded_vispy_modules('vispy.gloo.gl.pyopengl', 2, True)
+    """ Importing vispy.gloo.gl.pyopengl2 should import PyOpenGL. """
+    allmodnames = loaded_vispy_modules('vispy.gloo.gl.pyopengl2', 2, True)
     assert_in('OpenGL', allmodnames)
 
 
 def test_import_vispy_scene():
     """ Importing vispy.gloo.gl.desktop should not import PyOpenGL. """
     modnames = loaded_vispy_modules('vispy.scene', 2)
-    more_modules = ['vispy.app', 'vispy.gloo', 'vispy.scene', 'vispy.color', 
-                    'vispy.io', 'vispy.geometry']
+    more_modules = ['vispy.app', 'vispy.gloo', 'vispy.glsl', 'vispy.scene', 
+                    'vispy.color', 
+                    'vispy.io', 'vispy.geometry', 'vispy.visuals']
     assert_equal(modnames, set(_min_modules + more_modules))
+
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_key.py b/vispy/util/tests/test_key.py
index 87679f6..901b39b 100644
--- a/vispy/util/tests/test_key.py
+++ b/vispy/util/tests/test_key.py
@@ -1,6 +1,10 @@
-from nose.tools import assert_raises, assert_true, assert_equal
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from vispy.util.keys import Key, ENTER
+from vispy.testing import (run_tests_if_main, assert_raises, assert_true,
+                           assert_equal)
 
 
 def test_key():
@@ -13,3 +17,6 @@ def test_key():
     print(ENTER.name)
     print(ENTER)  # __repr__
     assert_equal(Key('1'), 49)  # ASCII code
+
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_logging.py b/vispy/util/tests/test_logging.py
index b6a158a..37de805 100644
--- a/vispy/util/tests/test_logging.py
+++ b/vispy/util/tests/test_logging.py
@@ -1,8 +1,11 @@
-from nose.tools import assert_equal
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import logging
 
 from vispy.util import logger, use_log_level
-from vispy.testing import assert_in, assert_not_in
+from vispy.testing import (assert_in, assert_not_in, run_tests_if_main,
+                           assert_equal)
 
 
 def test_logging():
@@ -37,3 +40,6 @@ def test_debug_logging():
         logger.info('bar')
     assert_equal(len(l), 1)
     assert_not_in('unknown', l[0])
+
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_run.py b/vispy/util/tests/test_run.py
index db43ff3..251ccd5 100644
--- a/vispy/util/tests/test_run.py
+++ b/vispy/util/tests/test_run.py
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
-
-from nose.tools import assert_raises
-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 from vispy.util import run_subprocess
+from vispy.testing import run_tests_if_main, assert_raises
 
 
 def test_run():
@@ -10,3 +10,6 @@ def test_run():
     """
     bad_name = 'foo_nonexist_test'
     assert_raises(Exception, run_subprocess, [bad_name])
+
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_transforms.py b/vispy/util/tests/test_transforms.py
index ac266ca..2f2240e 100644
--- a/vispy/util/tests/test_transforms.py
+++ b/vispy/util/tests/test_transforms.py
@@ -1,27 +1,31 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import numpy as np
-from nose.tools import assert_equal
 from numpy.testing import assert_allclose
 
-from vispy.util.transforms import (translate, scale, xrotate, yrotate,
-                                   zrotate, rotate, ortho, frustum,
+from vispy.util.transforms import (translate, scale, rotate, ortho, frustum,
                                    perspective)
+from vispy.testing import run_tests_if_main, assert_equal
 
 
 def test_transforms():
     """Test basic transforms"""
     xfm = np.random.randn(4, 4).astype(np.float32)
 
-    for rot in [xrotate, yrotate, zrotate]:
-        new_xfm = rot(rot(xfm, 90), -90)
-        assert_allclose(xfm, new_xfm)
-
-    new_xfm = rotate(rotate(xfm, 90, 1, 0, 0), 90, -1, 0, 0)
+    # Do a series of rotations that should end up into the same orientation
+    # again, to ensure the order of computation is all correct
+    # i.e. if rotated would return the transposed matrix this would not work
+    # out (the translation part would be incorrect)
+    new_xfm = xfm.dot(rotate(180, (1, 0, 0)).dot(rotate(-90, (0, 1, 0))))
+    new_xfm = new_xfm.dot(rotate(90, (0, 0, 1)).dot(rotate(90, (0, 1, 0))))
+    new_xfm = new_xfm.dot(rotate(90, (1, 0, 0)))
     assert_allclose(xfm, new_xfm)
 
-    new_xfm = translate(translate(xfm, 1, -1), 1, -1, 1)
+    new_xfm = translate((1, -1, 1)).dot(translate((-1, 1, -1))).dot(xfm)
     assert_allclose(xfm, new_xfm)
 
-    new_xfm = scale(scale(xfm, 1, 2, 3), 1, 1. / 2., 1. / 3.)
+    new_xfm = scale((1, 2, 3)).dot(scale((1, 1. / 2., 1. / 3.))).dot(xfm)
     assert_allclose(xfm, new_xfm)
 
     # These could be more complex...
@@ -33,3 +37,6 @@ def test_transforms():
 
     xfm = perspective(1, 1, -1, 1)
     assert_equal(xfm.shape, (4, 4))
+
+
+run_tests_if_main()
diff --git a/vispy/util/tests/test_vispy.py b/vispy/util/tests/test_vispy.py
index aec7313..476fe32 100644
--- a/vispy/util/tests/test_vispy.py
+++ b/vispy/util/tests/test_vispy.py
@@ -1,11 +1,13 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """ Tests to ensure that base vispy namespace functions correctly,
 including configuration options.
 """
 
-from nose.tools import assert_raises, assert_equal, assert_not_equal
-
 import vispy.app
-from vispy.testing import requires_application
+from vispy.testing import (requires_application, run_tests_if_main,
+                           assert_raises, assert_equal, assert_not_equal)
 
 
 @requires_application('pyside')
@@ -20,11 +22,11 @@ def test_use():
     
     try:
         # With no arguments, should do nothing
-        vispy.use()
+        assert_raises(TypeError, vispy.use)
         assert_equal(vispy.app._default_app.default_app, None)
         
         # With only gl args, should do nothing to app
-        vispy.use(gl='desktop')
+        vispy.use(gl='gl2')
         assert_equal(vispy.app._default_app.default_app, None)
         
         # Specify app (one we know works)
@@ -32,12 +34,15 @@ def test_use():
         assert_not_equal(vispy.app._default_app.default_app, None)
         
         # Again, but now wrong app
-        wrong_name = 'glut' if app_name.lower() != 'glut' else 'pyglet'
+        wrong_name = 'glfw' if app_name.lower() != 'glfw' else 'pyqt4'
         assert_raises(RuntimeError, vispy.use, wrong_name)
         
         # And both
-        vispy.use(app_name, 'desktop')
+        vispy.use(app_name, 'gl2')
     
     finally:
         # Restore
         vispy.app._default_app.default_app = default_app
+
+
+run_tests_if_main()
diff --git a/vispy/util/transforms.py b/vispy/util/transforms.py
index 4ef6e49..6314826 100644
--- a/vispy/util/transforms.py
+++ b/vispy/util/transforms.py
@@ -2,194 +2,78 @@
 # -*- coding: utf-8 -*-
 """
 Very simple transformation library that is needed for some examples.
-
-Note that functions that take a matrix as input generally operate on that
-matrix in place.
 """
 
+from __future__ import division
+
 # Note: we use functions (e.g. sin) from math module because they're faster
 
 import math
 import numpy as np
 
 
-def translate(M, x, y=None, z=None):
+def translate(offset, dtype=None):
     """Translate by an offset (x, y, z) .
 
     Parameters
     ----------
-    M : array
-        Original transformation (4x4).
-    x : float
-        X coordinate of a translation vector.
-    y : float | None
-        Y coordinate of translation vector. If None, `x` will be used.
-    z : float | None
-        Z coordinate of translation vector. If None, `x` will be used.
+    offset : array-like, shape (3,)
+        Translation in x, y, z.
+    dtype : dtype | None
+        Output type (if None, don't cast).
 
     Returns
     -------
-    M : array
-        Updated transformation (4x4). Note that this function operates
-        in-place.
+    M : ndarray
+        Transformation matrix describing the translation.
     """
-    y = x if y is None else y
-    z = x if z is None else z
-    T = np.array([[1.0, 0.0, 0.0, x],
-                  [0.0, 1.0, 0.0, y],
-                  [0.0, 0.0, 1.0, z],
-                  [0.0, 0.0, 0.0, 1.0]], dtype=M.dtype).T
-    M[...] = np.dot(M, T)
+    assert len(offset) == 3
+    x, y, z = offset
+    M = np.array([[1., 0., 0., 0.],
+                 [0., 1., 0., 0.],
+                 [0., 0., 1., 0.],
+                 [x, y, z, 1.0]], dtype)
     return M
 
 
-def scale(M, x, y=None, z=None):
+def scale(s, dtype=None):
     """Non-uniform scaling along the x, y, and z axes
 
     Parameters
     ----------
-    M : array
-        Original transformation (4x4).
-    x : float
-        X coordinate of the translation vector.
-    y : float | None
-        Y coordinate of the translation vector. If None, `x` will be used.
-    z : float | None
-        Z coordinate of the translation vector. If None, `x` will be used.
-
-    Returns
-    -------
-    M : array
-        Updated transformation (4x4). Note that this function operates
-        in-place.
-    """
-    y = x if y is None else y
-    z = x if z is None else z
-    S = np.array([[x, 0.0, 0.0, 0.0],
-                  [0.0, y, 0.0, 0.0],
-                  [0.0, 0.0, z, 0.0],
-                  [0.0, 0.0, 0.0, 1.0]], dtype=M.dtype).T
-    M[...] = np.dot(M, S)
-    return M
-
-
-def xrotate(M, theta):
-    """Rotate about the X axis
-
-    Parameters
-    ----------
-    M : array
-        Original transformation (4x4).
-    theta : float
-        Specifies the angle of rotation, in degrees.
+    s : array-like, shape (3,)
+        Scaling in x, y, z.
+    dtype : dtype | None
+        Output type (if None, don't cast).
 
     Returns
     -------
-    M : array
-        Updated transformation (4x4). Note that this function operates
-        in-place.
+    M : ndarray
+        Transformation matrix describing the scaling.
     """
-    t = math.pi * theta / 180.
-    cosT = math.cos(t)
-    sinT = math.sin(t)
-    R = np.array([[1.0, 0.0, 0.0, 0.0],
-                  [0.0, cosT, -sinT, 0.0],
-                  [0.0, sinT, cosT, 0.0],
-                  [0.0, 0.0, 0.0, 1.0]], dtype=M.dtype)
-    M[...] = np.dot(M, R)
-    return M
+    assert len(s) == 3
+    return np.array(np.diag(np.concatenate([s, (1.,)])), dtype)
 
 
-def yrotate(M, theta):
-    """Rotate about the Y axis
+def rotate(angle, axis, dtype=None):
+    """The 3x3 rotation matrix for rotation about a vector.
 
     Parameters
     ----------
-    M : array
-        Original transformation (4x4).
-    theta : float
-        Specifies the angle of rotation, in degrees.
-
-    Returns
-    -------
-    M : array
-        Updated transformation (4x4). Note that this function operates
-        in-place.
-    """
-    t = math.pi * theta / 180
-    cosT = math.cos(t)
-    sinT = math.sin(t)
-    R = np.array(
-        [[cosT, 0.0, sinT, 0.0],
-         [0.0, 1.0, 0.0, 0.0],
-         [-sinT, 0.0, cosT, 0.0],
-         [0.0, 0.0, 0.0, 1.0]], dtype=M.dtype)
-    M[...] = np.dot(M, R)
-    return M
-
-
-def zrotate(M, theta):
-    """Rotate about the Z axis
-
-    Parameters
-    ----------
-    M : array
-        Original transformation (4x4).
-    theta : float
-        Specifies the angle of rotation, in degrees.
-
-    Returns
-    -------
-    M : array
-        Updated transformation (4x4). Note that this function operates
-        in-place.
-    """
-    t = math.pi * theta / 180
-    cosT = math.cos(t)
-    sinT = math.sin(t)
-    R = np.array(
-        [[cosT, -sinT, 0.0, 0.0],
-         [sinT, cosT, 0.0, 0.0],
-         [0.0, 0.0, 1.0, 0.0],
-         [0.0, 0.0, 0.0, 1.0]], dtype=M.dtype)
-    M[...] = np.dot(M, R)
-    return M
-
-
-def rotate(M, angle, x, y, z, point=None):
-    """Rotation about a vector
-
-    Parameters
-    ----------
-    M : array
-        Original transformation (4x4).
     angle : float
-        Specifies the angle of rotation, in degrees.
-    x : float
-        X coordinate of the angle of rotation vector.
-    y : float | None
-        Y coordinate of the angle of rotation vector.
-    z : float | None
-        Z coordinate of the angle of rotation vector.
-
-    Returns
-    -------
-    M : array
-        Updated transformation (4x4). Note that this function operates
-        in-place.
+        The angle of rotation, in degrees.
+    axis : ndarray
+        The x, y, z coordinates of the axis direction vector.
     """
-    angle = math.pi * angle / 180
+    angle = np.radians(angle)
+    assert len(axis) == 3
+    x, y, z = axis / np.linalg.norm(axis)
     c, s = math.cos(angle), math.sin(angle)
-    n = math.sqrt(x * x + y * y + z * z)
-    x /= n
-    y /= n
-    z /= n
     cx, cy, cz = (1 - c) * x, (1 - c) * y, (1 - c) * z
-    R = np.array([[cx * x + c, cy * x - z * s, cz * x + y * s, 0],
-                  [cx * y + z * s, cy * y + c, cz * y - x * s, 0],
-                  [cx * z - y * s, cy * z + x * s, cz * z + c, 0],
-                  [0, 0, 0, 1]], dtype=M.dtype).T
-    M[...] = np.dot(M, R)
+    M = np.array([[cx * x + c, cy * x - z * s, cz * x + y * s, .0],
+                  [cx * y + z * s, cy * y + c, cz * y - x * s, 0.],
+                  [cx * z - y * s, cy * z + x * s, cz * z + c, 0.],
+                  [0., 0., 0., 1.]], dtype).T
     return M
 
 
@@ -213,7 +97,7 @@ def ortho(left, right, bottom, top, znear, zfar):
 
     Returns
     -------
-    M : array
+    M : ndarray
         Orthographic projection matrix (4x4).
     """
     assert(right != left)
@@ -251,7 +135,7 @@ def frustum(left, right, bottom, top, znear, zfar):
 
     Returns
     -------
-    M : array
+    M : ndarray
         View frustum matrix (4x4).
     """
     assert(right != left)
@@ -259,12 +143,12 @@ def frustum(left, right, bottom, top, znear, zfar):
     assert(znear != zfar)
 
     M = np.zeros((4, 4), dtype=np.float32)
-    M[0, 0] = +2.0 * znear / (right - left)
-    M[2, 0] = (right + left) / (right - left)
-    M[1, 1] = +2.0 * znear / (top - bottom)
-    M[3, 1] = (top + bottom) / (top - bottom)
-    M[2, 2] = -(zfar + znear) / (zfar - znear)
-    M[3, 2] = -2.0 * znear * zfar / (zfar - znear)
+    M[0, 0] = +2.0 * znear / float(right - left)
+    M[2, 0] = (right + left) / float(right - left)
+    M[1, 1] = +2.0 * znear / float(top - bottom)
+    M[2, 1] = (top + bottom) / float(top - bottom)
+    M[2, 2] = -(zfar + znear) / float(zfar - znear)
+    M[3, 2] = -2.0 * znear * zfar / float(zfar - znear)
     M[2, 3] = -1.0
     return M
 
@@ -285,7 +169,7 @@ def perspective(fovy, aspect, znear, zfar):
 
     Returns
     -------
-    M : array
+    M : ndarray
         Perspective projection matrix (4x4).
     """
     assert(znear != zfar)
@@ -296,19 +180,19 @@ def perspective(fovy, aspect, znear, zfar):
 
 def affine_map(points1, points2):
     """ Find a 3D transformation matrix that maps points1 onto points2.
-    
+
     Arguments are specified as arrays of four 3D coordinates, shape (4, 3).
     """
     A = np.ones((4, 4))
     A[:, :3] = points1
     B = np.ones((4, 4))
     B[:, :3] = points2
-    
+
     # solve 3 sets of linear equations to determine
     # transformation matrix elements
     matrix = np.eye(4)
     for i in range(3):
         # solve Ax = B; x is one row of the desired transformation matrix
-        matrix[i] = np.linalg.solve(A, B[:, i]) 
-    
+        matrix[i] = np.linalg.solve(A, B[:, i])
+
     return matrix
diff --git a/vispy/util/wrappers.py b/vispy/util/wrappers.py
index a344c77..bcdfafc 100644
--- a/vispy/util/wrappers.py
+++ b/vispy/util/wrappers.py
@@ -1,11 +1,9 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 Some wrappers to avoid circular imports, or make certain calls easier.
-"""
 
-"""
 The idea of a 'global' vispy.use function is that although vispy.app
 and vispy.gloo.gl can be used independently, they are not complely
 independent for some configureation. E.g. when using real ES 2.0,
@@ -33,26 +31,25 @@ def use(app=None, gl=None):
     app : str
         The app backend to use (case insensitive). Standard backends:
             * 'PyQt4': use Qt widget toolkit via PyQt4.
+            * 'PyQt5': use Qt widget toolkit via PyQt5.
             * 'PySide': use Qt widget toolkit via PySide.
             * 'PyGlet': use Pyglet backend.
             * 'Glfw': use Glfw backend (successor of Glut). Widely available
               on Linux.
             * 'SDL2': use SDL v2 backend.
-            * 'Glut': use Glut backend. Widely available but limited.
-              Not recommended.
         Additional backends:
             * 'ipynb_vnc': render in the IPython notebook via a VNC approach
               (experimental)
     gl : str
         The gl backend to use (case insensitive). Options are:
-            * 'desktop': use Vispy's desktop OpenGL API.
-            * 'pyopengl': use PyOpenGL's desktop OpenGL API. Mostly for
+            * 'gl2': use Vispy's desktop OpenGL API.
+            * 'pyopengl2': use PyOpenGL's desktop OpenGL API. Mostly for
               testing.
-            * 'angle': (TO COME) use real OpenGL ES 2.0 on Windows via Angle.
+            * 'es2': (TO COME) use real OpenGL ES 2.0 on Windows via Angle.
               Availability of ES 2.0 is larger for Windows, since it relies
               on DirectX.
-            * If 'debug' is included in this argument, vispy will check for
-              errors after each gl command.
+            * 'gl+': use the full OpenGL functionality available on
+              your system (via PyOpenGL).
 
     Notes
     -----
@@ -68,6 +65,8 @@ def use(app=None, gl=None):
     'default_backend' provided in the vispy config. If still not
     succesful, it will try each backend in a predetermined order.
     """
+    if app is None and gl is None:
+        raise TypeError('Must specify at least one of "app" or "gl".')
 
     # Example for future. This wont work (yet).
     if app == 'ipynb_webgl':
@@ -75,33 +74,17 @@ def use(app=None, gl=None):
         gl = 'webgl'
 
     # Apply now
-    if app:
-        import vispy.app
-        vispy.app.use_app(app)
     if gl:
         import vispy.gloo
+        from vispy import config
+        config['gl_backend'] = gl
         vispy.gloo.gl.use_gl(gl)
+    if app:
+        import vispy.app
+        vispy.app.use_app(app)
 
 
-# Define test proxy function, so we don't have to import vispy.testing always
-def test(label='full', coverage=False, verbosity=1, *extra_args):
-    """Test vispy software
-
-    Parameters
-    ----------
-    label : str
-        Can be one of 'full', 'nose', 'nobackend', 'extra', 'lineendings',
-        'flake', or any backend name (e.g., 'qt').
-    coverage : bool
-        Produce coverage outputs (.coverage file).
-    verbosity : int
-        Verbosity level to use when running ``nose``.
-    """
-    from ..testing import _tester
-    return _tester(label, coverage, verbosity, extra_args)
-
-
-def run_subprocess(command):
+def run_subprocess(command, return_code=False, **kwargs):
     """Run command using subprocess.Popen
 
     Run command and wait for command to complete. If the return code was zero
@@ -113,6 +96,12 @@ def run_subprocess(command):
     ----------
     command : list of str
         Command to run as subprocess (see subprocess.Popen documentation).
+    return_code : bool
+        If True, the returncode will be returned, and no error checking
+        will be performed (so this function should always return without
+        error).
+    **kwargs : dict
+        Additional kwargs to pass to ``subprocess.Popen``.
 
     Returns
     -------
@@ -120,21 +109,30 @@ def run_subprocess(command):
         Stdout returned by the process.
     stderr : str
         Stderr returned by the process.
+    code : int
+        The command exit code. Only returned if ``return_code`` is True.
     """
     # code adapted with permission from mne-python
-    kwargs = dict(stderr=subprocess.PIPE, stdout=subprocess.PIPE)
-
-    p = subprocess.Popen(command, **kwargs)
-    stdout_, stderr = p.communicate()
-
-    output = (stdout_, stderr)
-    if p.returncode:
-        print(stdout_)
-        print(stderr)
+    use_kwargs = dict(stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+    use_kwargs.update(kwargs)
+
+    p = subprocess.Popen(command, **use_kwargs)
+    output = p.communicate()
+    
+    # communicate() may return bytes, str, or None depending on the kwargs 
+    # passed to Popen(). Convert all to unicode str:
+    output = ['' if s is None else s for s in output]
+    output = [s.decode('utf-8') if isinstance(s, bytes) else s for s in output]
+    output = tuple(output)
+    
+    if not return_code and p.returncode:
+        print(output[0])
+        print(output[1])
         err_fun = subprocess.CalledProcessError.__init__
         if 'output' in inspect.getargspec(err_fun).args:
             raise subprocess.CalledProcessError(p.returncode, command, output)
         else:
             raise subprocess.CalledProcessError(p.returncode, command)
-
+    if return_code:
+        output = output + (p.returncode,)
     return output
diff --git a/vispy/visuals/__init__.py b/vispy/visuals/__init__.py
new file mode 100644
index 0000000..4e4a1d6
--- /dev/null
+++ b/vispy/visuals/__init__.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+"""
+This module provides a library of Visual classes, which are drawable objects
+intended to encapsulate simple graphic objects such as lines, meshes, points,
+2D shapes, images, text, etc.
+
+These classes define only the OpenGL machinery and connot be used directly in
+a scenegraph. For scenegraph use, see the complementary Visual+Node classes
+defined in vispy.scene.
+"""
+
+from .cube import CubeVisual  # noqa
+from .ellipse import EllipseVisual  # noqa
+from .gridlines import GridLinesVisual  # noqa
+from .image import ImageVisual  # noqa
+from .histogram import HistogramVisual  # noqa
+from .isocurve import IsocurveVisual  # noqa
+from .isoline import IsolineVisual  # noqa
+from .isosurface import IsosurfaceVisual  # noqa
+from .line import LineVisual  # noqa
+from .line_plot import LinePlotVisual  # noqa
+from .markers import MarkersVisual, marker_types  # noqa
+from .mesh import MeshVisual  # noqa
+from .polygon import PolygonVisual  # noqa
+from .rectangle import RectangleVisual  # noqa
+from .regular_polygon import RegularPolygonVisual  # noqa
+from .spectrogram import SpectrogramVisual  # noqa
+from .surface_plot import SurfacePlotVisual  # noqa
+from .text import TextVisual  # noqa
+from .tube import TubeVisual  # noqa
+from .visual import Visual  # noqa
+from .volume import VolumeVisual  # noqa
+from .xyz_axis import XYZAxisVisual  # noqa
diff --git a/vispy/visuals/collections/__init__.py b/vispy/visuals/collections/__init__.py
new file mode 100644
index 0000000..de211ed
--- /dev/null
+++ b/vispy/visuals/collections/__init__.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Collections allow batch rendering of object of the same type:
+
+ - Points
+ - Line segments
+ - Polylines (paths)
+ - Raw Triangles
+ - Polygons
+
+Each collection has several modes:
+
+ - raw (point, segment, path, triangle, polygon)
+ - agg (point, segment, path, polygon)
+ - agg+ (path, polygon)
+
+Note: Storage of shared attributes requires non-clamped textures which is not
+      the case on all graphic cards. This means such shared attributes must be
+      normalized on CPU and scales back on GPU (in shader code).
+"""
+
+from . path_collection import PathCollection  # noqa
+from . point_collection import PointCollection  # noqa
+from . polygon_collection import PolygonCollection  # noqa
+from . segment_collection import SegmentCollection  # noqa
+from . triangle_collection import TriangleCollection  # noqa
diff --git a/vispy/visuals/collections/agg_fast_path_collection.py b/vispy/visuals/collections/agg_fast_path_collection.py
new file mode 100644
index 0000000..00c730e
--- /dev/null
+++ b/vispy/visuals/collections/agg_fast_path_collection.py
@@ -0,0 +1,226 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Antigrain Geometry Fast Path Collection
+
+This collection provides antialiased and accurate paths with caps and miter
+joins. It consume x4 more memory than regular lines and is a bit slower, but
+the quality of the output is worth the cost. Note that no control can be made
+on miter joins which may result in some glitches on screen.
+"""
+import numpy as np
+from vispy import glsl
+from vispy.gloo import gl
+from . collection import Collection
+from ..transforms import NullTransform
+
+
+class AggFastPathCollection(Collection):
+
+    """
+    Antigrain Geometry Fast Path Collection
+
+    This collection provides antialiased and accurate paths with caps and miter
+    joins. It consume x4 more memory than regular lines and is a bit slower,
+    but the quality of the output is worth the cost. Note that no control can
+    be made on miter joins which may result in some glitches on screen.
+    """
+
+    def __init__(self, user_dtype=None, transform=None,
+                 vertex=None, fragment=None, **kwargs):
+        """
+        Initialize the collection.
+
+        Parameters
+        ----------
+
+        user_dtype: list
+            The base dtype can be completed (appended) by the used_dtype. It
+            only make sense if user also provide vertex and/or fragment shaders
+
+        transform : string
+            GLSL Transform code defining the vec4 transform(vec3) function
+
+        vertex: string
+            Vertex shader code
+
+        fragment: string
+            Fragment  shader code
+
+        caps : string
+            'local', 'shared' or 'global'
+
+        color : string
+            'local', 'shared' or 'global'
+
+        linewidth : string
+            'local', 'shared' or 'global'
+
+        antialias : string
+            'local', 'shared' or 'global'
+        """
+
+        base_dtype = [('prev',       (np.float32, 3), '!local', (0, 0, 0)),
+                      ('curr',       (np.float32, 3), '!local', (0, 0, 0)),
+                      ('next',       (np.float32, 3), '!local', (0, 0, 0)),
+                      ('id',         (np.float32, 1), '!local', 0),
+                      ('color',      (np.float32, 4), 'global', (0, 0, 0, 1)),
+                      ('linewidth',  (np.float32, 1), 'global', 1),
+                      ('antialias',  (np.float32, 1), 'global', 1),
+                      ("viewport",   (np.float32, 4), 'global', (0, 0, 512, 512))]  # noqa
+        dtype = base_dtype
+        if user_dtype:
+            dtype.extend(user_dtype)
+
+        if vertex is None:
+            vertex = glsl.get('collections/agg-fast-path.vert')
+        if transform is None:
+            transform = NullTransform()
+        self.transform = transform        
+        if fragment is None:
+            fragment = glsl.get('collections/agg-fast-path.frag')
+
+        Collection.__init__(self, dtype=dtype, itype=None,
+                            mode="triangle_strip",
+                            vertex=vertex, fragment=fragment, **kwargs)
+
+        program = self._programs[0]
+        program.vert['transform'] = self.transform
+
+    def append(self, P, closed=False, itemsize=None, **kwargs):
+        """
+        Append a new set of vertices to the collection.
+
+        For kwargs argument, n is the number of vertices (local) or the number
+        of item (shared)
+
+        Parameters
+        ----------
+
+        P : np.array
+            Vertices positions of the path(s) to be added
+
+        closed: bool
+            Whether path(s) is/are closed
+
+        itemsize: int or None
+            Size of an individual path
+
+        caps : list, array or 2-tuple
+           Path start /end cap
+
+        color : list, array or 4-tuple
+           Path color
+
+        linewidth : list, array or float
+           Path linewidth
+
+        antialias : list, array or float
+           Path antialias area
+        """
+
+        itemsize = itemsize or len(P)
+        itemcount = len(P) / itemsize
+
+        P = P.reshape(itemcount, itemsize, 3)
+        if closed:
+            V = np.empty((itemcount, itemsize + 3), dtype=self.vtype)
+            # Apply default values on vertices
+            for name in self.vtype.names:
+                if name not in ['collection_index', 'prev', 'curr', 'next']:
+                    V[name][1:-2] = kwargs.get(name, self._defaults[name])
+            V['prev'][:, 2:-1] = P
+            V['prev'][:, 1] = V['prev'][:, -2]
+            V['curr'][:, 1:-2] = P
+            V['curr'][:, -2] = V['curr'][:, 1]
+            V['next'][:, 0:-3] = P
+            V['next'][:, -3] = V['next'][:, 0]
+            V['next'][:, -2] = V['next'][:, 1]
+        else:
+            V = np.empty((itemcount, itemsize + 2), dtype=self.vtype)
+            # Apply default values on vertices
+            for name in self.vtype.names:
+                if name not in ['collection_index', 'prev', 'curr', 'next']:
+                    V[name][1:-1] = kwargs.get(name, self._defaults[name])
+            V['prev'][:, 2:] = P
+            V['prev'][:, 1] = V['prev'][:, 2]
+            V['curr'][:, 1:-1] = P
+            V['next'][:, :-2] = P
+            V['next'][:, -2] = V['next'][:, -3]
+
+        V[:, 0] = V[:, 1]
+        V[:, -1] = V[:, -2]
+        V = V.ravel()
+        V = np.repeat(V, 2, axis=0)
+        V['id'] = np.tile([1, -1], len(V) / 2)
+        if closed:
+            V = V.reshape(itemcount, 2 * (itemsize + 3))
+        else:
+            V = V.reshape(itemcount, 2 * (itemsize + 2))
+        V["id"][:, :2] = 2, -2
+        V["id"][:, -2:] = 2, -2
+        V = V.ravel()
+
+        # Uniforms
+        if self.utype:
+            U = np.zeros(itemcount, dtype=self.utype)
+            for name in self.utype.names:
+                if name not in ["__unused__"]:
+                    U[name] = kwargs.get(name, self._defaults[name])
+        else:
+            U = None
+
+        Collection.append(self, vertices=V, uniforms=U,
+                          itemsize=2 * (itemsize + 2 + closed))
+
+    def bake(self, P, key='curr', closed=False, itemsize=None):
+        """
+        Given a path P, return the baked vertices as they should be copied in
+        the collection if the path has already been appended.
+
+        Example:
+        --------
+
+        paths.append(P)
+        P *= 2
+        paths['prev'][0] = bake(P,'prev')
+        paths['curr'][0] = bake(P,'curr')
+        paths['next'][0] = bake(P,'next')
+        """
+
+        itemsize = itemsize or len(P)
+        itemcount = len(P) / itemsize  # noqa
+        n = itemsize
+
+        if closed:
+            I = np.arange(n + 3)
+            if key == 'prev':
+                I -= 2
+                I[0], I[1], I[-1] = n - 1, n - 1, n - 1
+            elif key == 'next':
+                I[0], I[-3], I[-2], I[-1] = 1, 0, 1, 1
+            else:
+                I -= 1
+                I[0], I[-1], I[n + 1] = 0, 0, 0
+        else:
+            I = np.arange(n + 2)
+            if key == 'prev':
+                I -= 2
+                I[0], I[1], I[-1] = 0, 0, n - 2
+            elif key == 'next':
+                I[0], I[-1], I[-2] = 1, n - 1, n - 1
+            else:
+                I -= 1
+                I[0], I[-1] = 0, n - 1
+        I = np.repeat(I, 2)
+        return P[I]
+
+    def draw(self, mode="triangle_strip"):
+        """ Draw collection """
+
+        gl.glDepthMask(gl.GL_FALSE)
+        Collection.draw(self, mode)
+        gl.glDepthMask(gl.GL_TRUE)
diff --git a/vispy/visuals/collections/agg_path_collection.py b/vispy/visuals/collections/agg_path_collection.py
new file mode 100644
index 0000000..0776568
--- /dev/null
+++ b/vispy/visuals/collections/agg_path_collection.py
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Antigrain Geometry Path Collection
+
+This collection provides antialiased and accurate paths with caps and joins. It
+is memory hungry (x8) and slow (x.25) so it is to be used sparingly, mainly for
+thick paths where quality is critical.
+"""
+import numpy as np
+from vispy import glsl
+from vispy.gloo import gl
+from . collection import Collection
+from ..transforms import NullTransform
+
+
+class AggPathCollection(Collection):
+
+    """
+    Antigrain Geometry Path Collection
+
+    This collection provides antialiased and accurate paths with caps and
+    joins. It is memory hungry (x8) and slow (x.25) so it is to be used
+    sparingly, mainly for thick paths where quality is critical.
+    """
+
+    def __init__(self, user_dtype=None, transform=None,
+                 vertex=None, fragment=None, **kwargs):
+        """
+        Initialize the collection.
+
+        Parameters
+        ----------
+
+        user_dtype: list
+            The base dtype can be completed (appended) by the used_dtype. It
+            only make sense if user also provide vertex and/or fragment shaders
+
+        transform : Transform instance
+            Used to define the transform(vec4) function
+
+        vertex: string
+            Vertex shader code
+
+        fragment: string
+            Fragment  shader code
+
+        caps : string
+            'local', 'shared' or 'global'
+
+        join : string
+            'local', 'shared' or 'global'
+
+        color : string
+            'local', 'shared' or 'global'
+
+        miter_limit : string
+            'local', 'shared' or 'global'
+
+        linewidth : string
+            'local', 'shared' or 'global'
+
+        antialias : string
+            'local', 'shared' or 'global'
+        """
+
+        base_dtype = [('p0',         (np.float32, 3), '!local', (0, 0, 0)),
+                      ('p1',         (np.float32, 3), '!local', (0, 0, 0)),
+                      ('p2',         (np.float32, 3), '!local', (0, 0, 0)),
+                      ('p3',         (np.float32, 3), '!local', (0, 0, 0)),
+                      ('uv',         (np.float32, 2), '!local', (0, 0)),
+
+                      ('caps',       (np.float32, 2), 'global', (0, 0)),
+                      ('join',       (np.float32, 1), 'global', 0),
+                      ('color',      (np.float32, 4), 'global', (0, 0, 0, 1)),
+                      ('miter_limit', (np.float32, 1), 'global', 4),
+                      ('linewidth',  (np.float32, 1), 'global', 1),
+                      ('antialias',  (np.float32, 1), 'global', 1),
+                      ('viewport',   (np.float32, 4), 'global', (0, 0, 512, 512))]  # noqa
+
+        dtype = base_dtype
+        if user_dtype:
+            dtype.extend(user_dtype)
+
+        if vertex is None:
+            vertex = glsl.get('collections/agg-path.vert')
+        if transform is None:
+            transform = NullTransform()
+        self.transform = transform        
+        if fragment is None:
+            fragment = glsl.get('collections/agg-path.frag')
+
+        Collection.__init__(self, dtype=dtype, itype=np.uint32,  # 16 for WebGL
+                            mode="triangles",
+                            vertex=vertex, fragment=fragment, **kwargs)
+        self._programs[0].vert['transform'] = self.transform
+
+    def append(self, P, closed=False, itemsize=None, **kwargs):
+        """
+        Append a new set of vertices to the collection.
+
+        For kwargs argument, n is the number of vertices (local) or the number
+        of item (shared)
+
+        Parameters
+        ----------
+
+        P : np.array
+            Vertices positions of the path(s) to be added
+
+        closed: bool
+            Whether path(s) is/are closed
+
+        itemsize: int or None
+            Size of an individual path
+
+        caps : list, array or 2-tuple
+           Path start /end cap
+
+        join : list, array or float
+           path segment join
+
+        color : list, array or 4-tuple
+           Path color
+
+        miter_limit : list, array or float
+           Miter limit for join
+
+        linewidth : list, array or float
+           Path linewidth
+
+        antialias : list, array or float
+           Path antialias area
+        """
+
+        itemsize = itemsize or len(P)
+        itemcount = len(P) / itemsize
+
+        # Computes the adjacency information
+        n, p = len(P), P.shape[-1]
+        Z = np.tile(P, 2).reshape(2 * len(P), p)
+        V = np.empty(n, dtype=self.vtype)
+
+        V['p0'][1:-1] = Z[0::2][:-2]
+        V['p1'][:-1] = Z[1::2][:-1]
+        V['p2'][:-1] = Z[1::2][+1:]
+        V['p3'][:-2] = Z[0::2][+2:]
+
+        # Apply default values on vertices
+        for name in self.vtype.names:
+            if name not in ['collection_index', 'p0', 'p1', 'p2', 'p3']:
+                V[name] = kwargs.get(name, self._defaults[name])
+
+        # Extract relevant segments only
+        V = (V.reshape(n / itemsize, itemsize)[:, :-1])
+        if closed:
+            V['p0'][:, 0] = V['p2'][:, -1]
+            V['p3'][:, -1] = V['p1'][:, 0]
+        else:
+            V['p0'][:, 0] = V['p1'][:, 0]
+            V['p3'][:, -1] = V['p2'][:, -1]
+        V = V.ravel()
+
+        # Quadruple each point (we're using 2 triangles / segment)
+        # No shared vertices between segment because of joins
+        V = np.repeat(V, 4, axis=0).reshape((len(V), 4))
+        V['uv'] = (-1, -1), (-1, +1), (+1, -1), (+1, +1)
+        V = V.ravel()
+
+        n = itemsize
+        if closed:
+            # uint16 for WebGL
+            I = np.resize(
+                np.array([0, 1, 2, 1, 2, 3], dtype=np.uint32), n * 2 * 3)
+            I += np.repeat(4 * np.arange(n), 6)
+            I[-6:] = 4 * n - 6, 4 * n - 5, 0, 4 * n - 5, 0, 1
+        else:
+            I = np.resize(
+                np.array([0, 1, 2, 1, 2, 3], dtype=np.uint32), (n - 1) * 2 * 3)
+            I += np.repeat(4 * np.arange(n - 1), 6)
+        I = I.ravel()
+
+        # Uniforms
+        if self.utype:
+            U = np.zeros(itemcount, dtype=self.utype)
+            for name in self.utype.names:
+                if name not in ["__unused__"]:
+                    U[name] = kwargs.get(name, self._defaults[name])
+        else:
+            U = None
+
+        Collection.append(self, vertices=V, uniforms=U,
+                          indices=I, itemsize=itemsize * 4 - 4)
+
+    def draw(self, mode="triangles"):
+        """ Draw collection """
+
+        gl.glDepthMask(0)
+        Collection.draw(self, mode)
+        gl.glDepthMask(1)
diff --git a/vispy/visuals/collections/agg_point_collection.py b/vispy/visuals/collections/agg_point_collection.py
new file mode 100644
index 0000000..5c1b8f4
--- /dev/null
+++ b/vispy/visuals/collections/agg_point_collection.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Antigrain Geometry Point Collection
+
+This collection provides fast points. Output quality is perfect.
+"""
+from vispy import glsl
+from . raw_point_collection import RawPointCollection
+
+
+class AggPointCollection(RawPointCollection):
+
+    """
+    Antigrain Geometry Point Collection
+
+    This collection provides fast points. Output quality is perfect.
+    """
+
+    def __init__(self, user_dtype=None, transform=None,
+                 vertex=None, fragment=None, **kwargs):
+        """
+        Initialize the collection.
+
+        Parameters
+        ----------
+
+        user_dtype: list
+            The base dtype can be completed (appended) by the used_dtype. It
+            only make sense if user also provide vertex and/or fragment shaders
+
+        vertex: string
+            Vertex shader code
+
+        fragment: string
+            Fragment  shader code
+
+        transform : Transform instance
+            Used to define the GLSL transform(vec4) function
+
+        color : string
+            'local', 'shared' or 'global'
+        """
+        if vertex is None:
+            vertex = glsl.get("collections/agg-point.vert")
+        if fragment is None:
+            fragment = glsl.get("collections/agg-point.frag")
+
+        RawPointCollection.__init__(self, user_dtype=user_dtype,
+                                    transform=transform,
+                                    vertex=vertex, fragment=fragment, **kwargs)
diff --git a/vispy/visuals/collections/agg_segment_collection.py b/vispy/visuals/collections/agg_segment_collection.py
new file mode 100644
index 0000000..46b583d
--- /dev/null
+++ b/vispy/visuals/collections/agg_segment_collection.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Antigrain Geometry Segment Collection
+
+This collection provides antialiased and accurate segments with caps. It
+consume x2 more memory than regular lines and is a bit slower, but the quality
+of the output is worth the cost.
+"""
+import numpy as np
+from vispy import glsl
+from . collection import Collection
+from ..transforms import NullTransform
+
+
+class AggSegmentCollection(Collection):
+
+    """
+    Antigrain Geometry Segment Collection
+
+    This collection provides antialiased and accurate segments with caps. It
+    consume x2 more memory than regular lines and is a bit slower, but the
+    quality of the output is worth the cost.
+    """
+
+    def __init__(self, user_dtype=None, transform=None,
+                 vertex=None, fragment=None, **kwargs):
+        """
+        Initialize the collection.
+
+        Parameters
+        ----------
+
+        user_dtype: list
+            The base dtype can be completed (appended) by the used_dtype. It
+            only make sense if user also provide vertex and/or fragment shaders
+
+        transform : string
+            GLSL Transform code defining the vec4 transform(vec3) function
+
+        vertex: string
+            Vertex shader code
+
+        fragment: string
+            Fragment  shader code
+
+        caps : string
+            'local', 'shared' or 'global'
+
+        color : string
+            'local', 'shared' or 'global'
+
+        linewidth : string
+            'local', 'shared' or 'global'
+
+        antialias : string
+            'local', 'shared' or 'global'
+        """
+
+        base_dtype = [('P0',        (np.float32, 3), '!local', (0, 0, 0)),
+                      ('P1',        (np.float32, 3), '!local', (0, 0, 0)),
+                      ('index',     (np.float32, 1), '!local', 0),
+                      ('color',     (np.float32, 4), 'shared', (0, 0, 0, 1)),
+                      ('linewidth', (np.float32, 1), 'shared', 1),
+                      ('antialias', (np.float32, 1), 'shared', 1),
+                      ('viewport',  (np.float32, 4), 'global', (0, 0, 512, 512))]  # noqa
+
+        dtype = base_dtype
+        if user_dtype:
+            dtype.extend(user_dtype)
+
+        if vertex is None:
+            vertex = glsl.get('collections/agg-segment.vert')
+        if transform is None:
+            transform = NullTransform()
+        self.transform = transform        
+        if fragment is None:
+            fragment = glsl.get('collections/agg-segment.frag')
+
+        Collection.__init__(self, dtype=dtype, itype=np.uint32,
+                            mode="triangles",
+                            vertex=vertex, fragment=fragment, **kwargs)
+        self._programs[0].vert['transform'] = self.transform
+
+    def append(self, P0, P1, itemsize=None, **kwargs):
+        """
+        Append a new set of segments to the collection.
+
+        For kwargs argument, n is the number of vertices (local) or the number
+        of item (shared)
+
+        Parameters
+        ----------
+
+        P : np.array
+            Vertices positions of the path(s) to be added
+
+        itemsize: int or None
+            Size of an individual path
+
+        caps : list, array or 2-tuple
+           Path start /end cap
+
+        color : list, array or 4-tuple
+           Path color
+
+        linewidth : list, array or float
+           Path linewidth
+
+        antialias : list, array or float
+           Path antialias area
+        """
+
+        itemsize = itemsize or 1
+        itemcount = len(P0) // itemsize
+
+        V = np.empty(itemcount, dtype=self.vtype)
+
+        # Apply default values on vertices
+        for name in self.vtype.names:
+            if name not in ['collection_index', 'P0', 'P1', 'index']:
+                V[name] = kwargs.get(name, self._defaults[name])
+
+        V['P0'] = P0
+        V['P1'] = P1
+        V = V.repeat(4, axis=0)
+        V['index'] = np.resize([0, 1, 2, 3], 4 * itemcount * itemsize)
+
+        I = np.ones((itemcount, 6), dtype=int)
+        I[:] = 0, 1, 2, 0, 2, 3
+        I[:] += 4 * np.arange(itemcount)[:, np.newaxis]
+        I = I.ravel()
+
+        # Uniforms
+        if self.utype:
+            U = np.zeros(itemcount, dtype=self.utype)
+            for name in self.utype.names:
+                if name not in ["__unused__"]:
+                    U[name] = kwargs.get(name, self._defaults[name])
+        else:
+            U = None
+
+        Collection.append(
+            self, vertices=V, uniforms=U, indices=I, itemsize=4 * itemcount)
diff --git a/vispy/visuals/collections/array_list.py b/vispy/visuals/collections/array_list.py
new file mode 100644
index 0000000..bff6d18
--- /dev/null
+++ b/vispy/visuals/collections/array_list.py
@@ -0,0 +1,415 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+"""
+An ArrayList is a strongly typed list whose type can be anything that can be
+interpreted as a numpy data type.
+
+Example
+-------
+
+>>> L = ArrayList( [[0], [1,2], [3,4,5], [6,7,8,9]] )
+>>> print L
+[ [0] [1 2] [3 4 5] [6 7 8 9] ]
+>>> print L.data
+[0 1 2 3 4 5 6 7 8 9]
+
+You can add several items at once by specifying common or individual size: a
+single scalar means all items are the same size while a list of sizes is used
+to specify individual item sizes.
+
+Example
+-------
+
+>>> L = ArrayList( np.arange(10), [3,3,4])
+>>> print L
+[ [0 1 2] [3 4 5] [6 7 8 9] ]
+>>> print L.data
+[0 1 2 3 4 5 6 7 8 9]
+"""
+import numpy as np
+
+
+class ArrayList(object):
+
+    """
+    An ArrayList is a strongly typed list whose type can be anything that can
+    be interpreted as a numpy data type.
+    """
+
+    def __init__(self, data=None, itemsize=None, dtype=float,
+                 sizeable=True, writeable=True):
+        """ Create a new buffer using given data and sizes or dtype
+
+        Parameters
+        ----------
+
+        data : array_like
+            An array, any object exposing the array interface, an object
+            whose __array__ method returns an array, or any (nested) sequence.
+
+        itemsize:  int or 1-D array
+            If `itemsize is an integer, N, the array will be divided
+            into elements of size N. If such partition is not possible,
+            an error is raised.
+
+            If `itemsize` is 1-D array, the array will be divided into
+            elements whose succesive sizes will be picked from itemsize.
+            If the sum of itemsize values is different from array size,
+            an error is raised.
+
+        dtype: np.dtype
+            Any object that can be interpreted as a numpy data type.
+
+        sizeable : boolean
+            Indicate whether item can be appended/inserted/deleted
+
+        writeable : boolean
+            Indicate whether content can be changed
+        """
+
+        self._sizeable = sizeable
+        self._writeable = writeable
+
+        if data is not None:
+            if isinstance(data, (list, tuple)):
+                if isinstance(data[0], (list, tuple)):
+                    itemsize = [len(l) for l in data]
+                    data = [item for sublist in data for item in sublist]
+            self._data = np.array(data, copy=False)
+            self._size = self._data.size
+
+            # Default is one group with all data inside
+            _itemsize = np.ones(1) * self._data.size
+
+            # Check item sizes and get items count
+            if itemsize is not None:
+                if isinstance(itemsize, int):
+                    if (self._size % itemsize) != 0:
+                        raise ValueError("Cannot partition data as requested")
+                    self._count = self._size // itemsize
+                    _itemsize = np.ones(
+                        self._count, dtype=int) * (self._size // self._count)
+                else:
+                    _itemsize = np.array(itemsize, copy=False)
+                    self._count = len(itemsize)
+                    if _itemsize.sum() != self._size:
+                        raise ValueError("Cannot partition data as requested")
+            else:
+                self._count = 1
+
+            # Store items
+            self._items = np.zeros((self._count, 2), int)
+            C = _itemsize.cumsum()
+            self._items[1:, 0] += C[:-1]
+            self._items[0:, 1] += C
+
+        else:
+            self._data = np.zeros(1, dtype=dtype)
+            self._items = np.zeros((1, 2), dtype=int)
+            self._size = 0
+            self._count = 0
+
+    @property
+    def data(self):
+        """ The array's elements, in memory. """
+        return self._data[:self._size]
+
+    @property
+    def size(self):
+        """ Number of base elements, in memory. """
+        return self._size
+
+    @property
+    def itemsize(self):
+        """ Individual item sizes """
+        return self._items[:self._count, 1] - self._items[:self._count, 0]
+
+    @property
+    def dtype(self):
+        """ Describes the format of the elements in the buffer. """
+        return self._data.dtype
+
+    def reserve(self, capacity):
+        """ Set current capacity of the underlying array"""
+
+        if capacity >= self._data.size:
+            capacity = int(2 ** np.ceil(np.log2(capacity)))
+            self._data = np.resize(self._data, capacity)
+
+    def __len__(self):
+        """ x.__len__() <==> len(x) """
+        return self._count
+
+    def __str__(self):
+        s = '[ '
+        for item in self:
+            s += str(item) + ' '
+        s += ']'
+        return s
+
+    def __getitem__(self, key):
+        """ x.__getitem__(y) <==> x[y] """
+
+        if isinstance(key, int):
+            if key < 0:
+                key += len(self)
+            if key < 0 or key >= len(self):
+                raise IndexError("Tuple index out of range")
+            dstart = self._items[key][0]
+            dstop = self._items[key][1]
+            return self._data[dstart:dstop]
+
+        elif isinstance(key, slice):
+            istart, istop, step = key.indices(len(self))
+            if istart > istop:
+                istart, istop = istop, istart
+            dstart = self._items[istart][0]
+            if istart == istop:
+                dstop = dstart
+            else:
+                dstop = self._items[istop - 1][1]
+            return self._data[dstart:dstop]
+
+        elif isinstance(key, str):
+            return self._data[key][:self._size]
+
+        elif key is Ellipsis:
+            return self.data
+
+        else:
+            raise TypeError("List indices must be integers")
+
+    def __setitem__(self, key, data):
+        """ x.__setitem__(i, y) <==> x[i]=y """
+
+        if not self._writeable:
+            raise AttributeError("List is not writeable")
+
+        if isinstance(key, (int, slice)):
+            if isinstance(key, int):
+                if key < 0:
+                    key += len(self)
+                if key < 0 or key > len(self):
+                    raise IndexError("List assignment index out of range")
+                dstart = self._items[key][0]
+                dstop = self._items[key][1]
+                istart = key
+            elif isinstance(key, slice):
+                istart, istop, step = key.indices(len(self))
+                if istart == istop:
+                    return
+                if istart > istop:
+                    istart, istop = istop, istart
+                if istart > len(self) or istop > len(self):
+                    raise IndexError("Can only assign iterable")
+                dstart = self._items[istart][0]
+                if istart == istop:
+                    dstop = dstart
+                else:
+                    dstop = self._items[istop - 1][1]
+
+            if hasattr(data, "__len__"):
+                if len(data) == dstop - dstart:  # or len(data) == 1:
+                    self._data[dstart:dstop] = data
+                else:
+                    self.__delitem__(key)
+                    self.insert(istart, data)
+            else:  # we assume len(data) = 1
+                if dstop - dstart == 1:
+                    self._data[dstart:dstop] = data
+                else:
+                    self.__delitem__(key)
+                    self.insert(istart, data)
+
+        elif key is Ellipsis:
+            self.data[...] = data
+
+        elif isinstance(key, str):
+            self._data[key][:self._size] = data
+
+        else:
+            raise TypeError("List assignment indices must be integers")
+
+    def __delitem__(self, key):
+        """ x.__delitem__(y) <==> del x[y] """
+
+        if not self._sizeable:
+            raise AttributeError("List is not sizeable")
+
+        # Deleting a single item
+        if isinstance(key, int):
+            if key < 0:
+                key += len(self)
+            if key < 0 or key > len(self):
+                raise IndexError("List deletion index out of range")
+            istart, istop = key, key + 1
+            dstart, dstop = self._items[key]
+
+        # Deleting several items
+        elif isinstance(key, slice):
+            istart, istop, step = key.indices(len(self))
+            if istart > istop:
+                istart, istop = istop, istart
+            if istart == istop:
+                return
+            dstart = self._items[istart][0]
+            dstop = self._items[istop - 1][1]
+
+        elif key is Ellipsis:
+            istart = 0
+            istop = len(self)
+            dstart = 0
+            dstop = self.size
+        # Error
+        else:
+            raise TypeError("List deletion indices must be integers")
+
+        # Remove data
+        size = self._size - (dstop - dstart)
+        self._data[
+            dstart:dstart + self._size - dstop] = self._data[dstop:self._size]
+        self._size -= dstop - dstart
+
+        # Remove corresponding items
+        size = self._count - istop
+        self._items[istart:istart + size] = self._items[istop:istop + size]
+
+        # Update other items
+        size = dstop - dstart
+        self._items[istart:istop + size + 1] -= size, size
+        self._count -= istop - istart
+
+    def insert(self, index, data, itemsize=None):
+        """ Insert data before index
+
+        Parameters
+        ----------
+
+        index : int
+            Index before which data will be inserted.
+
+        data : array_like
+            An array, any object exposing the array interface, an object
+            whose __array__ method returns an array, or any (nested) sequence.
+
+        itemsize:  int or 1-D array
+            If `itemsize is an integer, N, the array will be divided
+            into elements of size N. If such partition is not possible,
+            an error is raised.
+
+            If `itemsize` is 1-D array, the array will be divided into
+            elements whose succesive sizes will be picked from itemsize.
+            If the sum of itemsize values is different from array size,
+            an error is raised.
+        """
+
+        if not self._sizeable:
+            raise AttributeError("List is not sizeable")
+
+        if isinstance(data, (list, tuple)) and isinstance(data[0], (list, tuple)):  # noqa
+            itemsize = [len(l) for l in data]
+            data = [item for sublist in data for item in sublist]
+
+        data = np.array(data, copy=False).ravel()
+        size = data.size
+
+        # Check item size and get item number
+        if itemsize is not None:
+            if isinstance(itemsize, int):
+                if (size % itemsize) != 0:
+                    raise ValueError("Cannot partition data as requested")
+                _count = size // itemsize
+                _itemsize = np.ones(_count, dtype=int) * (size // _count)
+            else:
+                _itemsize = np.array(itemsize, copy=False)
+                _count = len(itemsize)
+                if _itemsize.sum() != size:
+                    raise ValueError("Cannot partition data as requested")
+        else:
+            _count = 1
+
+        # Check if data array is big enough and resize it if necessary
+        if self._size + size >= self._data.size:
+            capacity = int(2 ** np.ceil(np.log2(self._size + size)))
+            self._data = np.resize(self._data, capacity)
+
+        # Check if item array is big enough and resize it if necessary
+        if self._count + _count >= len(self._items):
+            capacity = int(2 ** np.ceil(np.log2(self._count + _count)))
+            self._items = np.resize(self._items, (capacity, 2))
+
+        # Check index
+        if index < 0:
+            index += len(self)
+        if index < 0 or index > len(self):
+            raise IndexError("List insertion index out of range")
+
+        # Inserting
+        if index < self._count:
+            istart = index
+            dstart = self._items[istart][0]
+            dstop = self._items[istart][1]
+            # Move data
+            Z = self._data[dstart:self._size]
+            self._data[dstart + size:self._size + size] = Z
+            # Update moved items
+            I = self._items[istart:self._count] + size
+            self._items[istart + _count:self._count + _count] = I
+
+        # Appending
+        else:
+            dstart = self._size
+            istart = self._count
+
+        # Only one item (faster)
+        if _count == 1:
+            # Store data
+            self._data[dstart:dstart + size] = data
+            self._size += size
+            # Store data location (= item)
+            self._items[istart][0] = dstart
+            self._items[istart][1] = dstart + size
+            self._count += 1
+
+        # Several items
+        else:
+            # Store data
+            dstop = dstart + size
+            self._data[dstart:dstop] = data
+            self._size += size
+
+            # Store items
+            items = np.ones((_count, 2), int) * dstart
+            C = _itemsize.cumsum()
+            items[1:, 0] += C[:-1]
+            items[0:, 1] += C
+            istop = istart + _count
+            self._items[istart:istop] = items
+            self._count += _count
+
+    def append(self, data, itemsize=None):
+        """
+        Append data to the end.
+
+        Parameters
+        ----------
+
+        data : array_like
+            An array, any object exposing the array interface, an object
+            whose __array__ method returns an array, or any (nested) sequence.
+
+        itemsize:  int or 1-D array
+            If `itemsize is an integer, N, the array will be divided
+            into elements of size N. If such partition is not possible,
+            an error is raised.
+
+            If `itemsize` is 1-D array, the array will be divided into
+            elements whose succesive sizes will be picked from itemsize.
+            If the sum of itemsize values is different from array size,
+            an error is raised.
+        """
+
+        self.insert(len(self), data, itemsize)
diff --git a/vispy/visuals/collections/base_collection.py b/vispy/visuals/collections/base_collection.py
new file mode 100644
index 0000000..b33875f
--- /dev/null
+++ b/vispy/visuals/collections/base_collection.py
@@ -0,0 +1,495 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+A collection is a container for several (optionally indexed) objects having
+the same vertex structure (vtype) and same uniforms type (utype). A collection
+allows to manipulate objects individually and each object can have its own set
+of uniforms provided they are a combination of floats.
+"""
+import math
+import numpy as np
+from vispy.gloo import Texture2D, VertexBuffer, IndexBuffer
+from . util import dtype_reduce
+from . array_list import ArrayList
+
+
+def next_power_of_2(n):
+    """ Return next power of 2 greater than or equal to n """
+    n -= 1  # greater than OR EQUAL TO n
+    shift = 1
+    while (n + 1) & n:  # n+1 is not a power of 2 yet
+        n |= n >> shift
+        shift *= 2
+    return max(4, n + 1)
+
+
+class Item(object):
+
+    """
+    An item represent an object within a collection and is created on demand
+    when accessing a specific object of the collection.
+    """
+
+    def __init__(self, parent, key, vertices, indices, uniforms):
+        """
+        Create an item from an existing collection.
+
+        Parameters
+        ----------
+
+        parent : Collection
+            Collection this item belongs to
+
+        key : int
+            Collection index of this item
+
+        vertices: array-like
+            Vertices of the item
+
+        indices: array-like
+            Indices of the item
+
+        uniforms: array-like
+            Uniform parameters of the item
+        """
+
+        self._parent = parent
+        self._key = key
+        self._vertices = vertices
+        self._indices = indices
+        self._uniforms = uniforms
+
+    @property
+    def vertices(self):
+        return self._vertices
+
+    @vertices.setter
+    def vertices(self, data):
+        self._vertices[...] = np.array(data)
+
+    @property
+    def indices(self):
+        return self._indices
+
+    @indices.setter
+    def indices(self, data):
+        if self._indices is None:
+            raise ValueError("Item has no indices")
+        start = self._parent.vertices._items[self._key][0]
+        self._indices[...] = np.array(data) + start
+
+    @property
+    def uniforms(self):
+        return self._uniforms
+
+    @uniforms.setter
+    def uniforms(self, data):
+        if self._uniforms is None:
+            raise ValueError("Item has no associated uniform")
+        self._uniforms[...] = data
+
+    def __getitem__(self, key):
+        """ Get a specific uniforms value """
+
+        if key in self._vertices.dtype.names:
+            return self._vertices[key]
+        elif key in self._uniforms.dtype.names:
+            return self._uniforms[key]
+        else:
+            raise IndexError("Unknown key ('%s')" % key)
+
+    def __setitem__(self, key, value):
+        """ Set a specific uniforms value """
+
+        if key in self._vertices.dtype.names:
+            self._vertices[key] = value
+        elif key in self._uniforms.dtype.names:
+            self._uniforms[key] = value
+        else:
+            raise IndexError("Unknown key")
+
+    def __str__(self):
+        return "Item (%s, %s, %s)" % (self._vertices,
+                                      self._indices,
+                                      self._uniforms)
+
+
+class BaseCollection(object):
+
+    def __init__(self, vtype, utype=None, itype=None):
+
+        # Vertices and type (mandatory)
+        self._vertices_list = None
+        self._vertices_buffer = None
+
+        # Vertex indices and type (optional)
+        self._indices_list = None
+        self._indices_buffer = None
+
+        # Uniforms and type (optional)
+        self._uniforms_list = None
+        self._uniforms_texture = None
+
+        # Make sure types are np.dtype (or None)
+        vtype = np.dtype(vtype) if vtype is not None else None
+        itype = np.dtype(itype) if itype is not None else None
+        utype = np.dtype(utype) if utype is not None else None
+
+        # Vertices type (mandatory)
+        # -------------------------
+        if vtype.names is None:
+            raise ValueError("vtype must be a structured dtype")
+
+        # Indices type (optional)
+        # -----------------------
+        if itype is not None:
+            if itype not in [np.uint8, np.uint16, np.uint32]:
+                raise ValueError("itype must be unsigned integer or None")
+            self._indices_list = ArrayList(dtype=itype)
+
+        # No program yet
+        self._programs = []
+
+        # Need to update buffers & texture
+        self._need_update = True
+
+        # Uniforms type (optional)
+        # -------------------------
+        if utype is not None:
+
+            if utype.names is None:
+                raise ValueError("utype must be a structured dtype")
+
+            # Convert types to lists (in case they were already dtypes) such
+            # that we can append new fields
+            vtype = eval(str(np.dtype(vtype)))
+            # We add a uniform index to access uniform data
+            vtype.append(('collection_index', 'f4'))
+            vtype = np.dtype(vtype)
+
+            # Check utype is made of float32 only
+            utype = eval(str(np.dtype(utype)))
+            r_utype = dtype_reduce(utype)
+            if type(r_utype[0]) is not str or r_utype[2] != 'float32':
+                raise RuntimeError("utype cannot be reduced to float32 only")
+
+            # Make utype divisible by 4
+            # count = ((r_utype[1]-1)//4+1)*4
+
+            # Make utype a power of two
+            count = next_power_of_2(r_utype[1])
+            if (count - r_utype[1]) > 0:
+                utype.append(('__unused__', 'f4', count - r_utype[1]))
+
+            self._uniforms_list = ArrayList(dtype=utype)
+            self._uniforms_float_count = count
+
+            # Reserve some space in texture such that we have
+            # at least one full line
+            shape = self._compute_texture_shape(1)
+            self._uniforms_list.reserve(shape[1] / (count / 4))
+
+        # Last since utype may add a new field in vtype (collecion_index)
+        self._vertices_list = ArrayList(dtype=vtype)
+
+        # Record all types
+        self._vtype = np.dtype(vtype)
+        self._itype = np.dtype(itype) if itype is not None else None
+        self._utype = np.dtype(utype) if utype is not None else None
+
+    def __len__(self):
+        """ x.__len__() <==> len(x) """
+
+        return len(self._vertices_list)
+
+    @property
+    def vtype(self):
+        """ Vertices dtype """
+
+        return self._vtype
+
+    @property
+    def itype(self):
+        """ Indices dtype """
+
+        return self._itype
+
+    @property
+    def utype(self):
+        """ Uniforms dtype """
+
+        return self._utype
+
+    def append(self, vertices, uniforms=None, indices=None, itemsize=None):
+        """
+        Parameters
+        ----------
+
+        vertices : numpy array
+            An array whose dtype is compatible with self.vdtype
+
+        uniforms: numpy array
+            An array whose dtype is compatible with self.utype
+
+        indices : numpy array
+            An array whose dtype is compatible with self.idtype
+            All index values must be between 0 and len(vertices)
+
+        itemsize: int, tuple or 1-D array
+            If `itemsize is an integer, N, the array will be divided
+            into elements of size N. If such partition is not possible,
+            an error is raised.
+
+            If `itemsize` is 1-D array, the array will be divided into
+            elements whose succesive sizes will be picked from itemsize.
+            If the sum of itemsize values is different from array size,
+            an error is raised.
+        """
+
+        # Vertices
+        # -----------------------------
+        vertices = np.array(vertices).astype(self.vtype).ravel()
+        vsize = self._vertices_list.size
+
+        # No itemsize given
+        # -----------------
+        if itemsize is None:
+            index = 0
+            count = 1
+
+        # Uniform itemsize (int)
+        # ----------------------
+        elif isinstance(itemsize, int):
+            count = len(vertices) / itemsize
+            index = np.repeat(np.arange(count), itemsize)
+
+        # Individual itemsize (array)
+        # ---------------------------
+        elif isinstance(itemsize, (np.ndarray, list)):
+            count = len(itemsize)
+            index = np.repeat(np.arange(count), itemsize)
+        else:
+            raise ValueError("Itemsize not understood")
+
+        if self.utype:
+            vertices["collection_index"] = index + len(self)
+        self._vertices_list.append(vertices, itemsize)
+
+        # Indices
+        # -----------------------------
+        if self.itype is not None:
+            # No indices given (-> automatic generation)
+            if indices is None:
+                indices = vsize + np.arange(len(vertices))
+                self._indices_list.append(indices, itemsize)
+
+            # Indices given
+            # FIXME: variables indices (list of list or ArrayList)
+            else:
+                if itemsize is None:
+                    I = np.array(indices) + vsize
+                elif isinstance(itemsize, int):
+                    I = vsize + (np.tile(indices, count) +
+                                 itemsize * np.repeat(np.arange(count), len(indices)))  # noqa
+                else:
+                    raise ValueError("Indices not compatible with items")
+                self._indices_list.append(I, len(indices))
+
+        # Uniforms
+        # -----------------------------
+        if self.utype:
+            if uniforms is None:
+                uniforms = np.zeros(count, dtype=self.utype)
+            else:
+                uniforms = np.array(uniforms).astype(self.utype).ravel()
+            self._uniforms_list.append(uniforms, itemsize=1)
+
+        self._need_update = True
+
+    def __delitem__(self, index):
+        """ x.__delitem__(y) <==> del x[y] """
+
+        # Deleting one item
+        if isinstance(index, int):
+            if index < 0:
+                index += len(self)
+            if index < 0 or index > len(self):
+                raise IndexError("Collection deletion index out of range")
+            istart, istop = index, index + 1
+        # Deleting several items
+        elif isinstance(index, slice):
+            istart, istop, _ = index.indices(len(self))
+            if istart > istop:
+                istart, istop = istop, istart
+            if istart == istop:
+                return
+        # Deleting everything
+        elif index is Ellipsis:
+            istart, istop = 0, len(self)
+        # Error
+        else:
+            raise TypeError("Collection deletion indices must be integers")
+
+        vsize = len(self._vertices_list[index])
+        if self.itype is not None:
+            del self._indices_list[index]
+            self._indices_list[index:] -= vsize
+
+        if self.utype:
+            self._vertices_list[index:]["collection_index"] -= istop - istart
+        del self._vertices_list[index]
+
+        if self.utype is not None:
+            del self._uniforms_list[index]
+
+        self._need_update = True
+
+    def __getitem__(self, key):
+        """ """
+
+        # WARNING
+        # Here we want to make sure to use buffers and texture (instead of
+        # lists) since only them are aware of any external modification.
+        if self._need_update:
+            self._update()
+
+        V = self._vertices_buffer
+        I = None
+        U = None
+        if self._indices_list is not None:
+            I = self._indices_buffer
+        if self._uniforms_list is not None:
+            U = self._uniforms_texture.data.ravel().view(self.utype)
+
+        # Getting a whole field
+        if isinstance(key, str):
+            # Getting a named field from vertices
+            if key in V.dtype.names:
+                return V[key]
+            # Getting a named field from uniforms
+            elif U is not None and key in U.dtype.names:
+                # Careful, U is the whole texture that can be bigger than list
+                # return U[key]
+                return U[key][:len(self._uniforms_list)]
+            else:
+                raise IndexError("Unknown field name ('%s')" % key)
+
+        # Getting individual item
+        elif isinstance(key, int):
+            vstart, vend = self._vertices_list._items[key]
+            vertices = V[vstart:vend]
+            indices = None
+            uniforms = None
+            if I is not None:
+                istart, iend = self._indices_list._items[key]
+                indices = I[istart:iend]
+
+            if U is not None:
+                ustart, uend = self._uniforms_list._items[key]
+                uniforms = U[ustart:uend]
+
+            return Item(self, key, vertices, indices, uniforms)
+
+        # Error
+        else:
+            raise IndexError("Cannot get more than one item at once")
+
+    def __setitem__(self, key, data):
+        """ x.__setitem__(i, y) <==> x[i]=y """
+
+        # if len(self._programs):
+        # found = False
+        # for program in self._programs:
+        #     if key in program.hooks:
+        #         program[key] = data
+        #         found = True
+        # if found: return
+
+        # WARNING
+        # Here we want to make sure to use buffers and texture (instead of
+        # lists) since only them are aware of any external modification.
+        if self._need_update:
+            self._update()
+
+        V = self._vertices_buffer
+        I = None
+        U = None
+        if self._indices_list is not None:
+            I = self._indices_buffer  # noqa
+        if self._uniforms_list is not None:
+            U = self._uniforms_texture.data.ravel().view(self.utype)
+
+        # Setting a whole field
+        if isinstance(key, str):
+            # Setting a named field in vertices
+            if key in self.vtype.names:
+                V[key] = data
+            # Setting a named field in uniforms
+            elif self.utype and key in self.utype.names:
+                # Careful, U is the whole texture that can be bigger than list
+                # U[key] = data
+                U[key][:len(self._uniforms_list)] = data
+            else:
+                raise IndexError("Unknown field name ('%s')" % key)
+
+        # # Setting individual item
+        # elif isinstance(key, int):
+        #     #vstart, vend = self._vertices_list._items[key]
+        #     #istart, iend = self._indices_list._items[key]
+        #     #ustart, uend = self._uniforms_list._items[key]
+        #     vertices, indices, uniforms = data
+        #     del self[key]
+        #     self.insert(key, vertices, indices, uniforms)
+
+        else:
+            raise IndexError("Cannot set more than one item")
+
+    def _compute_texture_shape(self, size=1):
+        """ Compute uniform texture shape """
+
+        # We should use this line but we may not have a GL context yet
+        # linesize = gl.glGetInteger(gl.GL_MAX_TEXTURE_SIZE)
+        linesize = 1024
+        count = self._uniforms_float_count
+        cols = linesize // float(count / 4)
+        rows = max(1, int(math.ceil(size / float(cols))))
+        shape = rows, cols * (count / 4), count
+        self._ushape = shape
+        return shape
+
+    def _update(self):
+        """ Update vertex buffers & texture """
+
+        if self._vertices_buffer is not None:
+            self._vertices_buffer.delete()
+        self._vertices_buffer = VertexBuffer(self._vertices_list.data)
+
+        if self.itype is not None:
+            if self._indices_buffer is not None:
+                self._indices_buffer.delete()
+            self._indices_buffer = IndexBuffer(self._indices_list.data)
+
+        if self.utype is not None:
+            if self._uniforms_texture is not None:
+                self._uniforms_texture.delete()
+
+            # We take the whole array (_data), not the data one
+            texture = self._uniforms_list._data.view(np.float32)
+            size = len(texture) / self._uniforms_float_count
+            shape = self._compute_texture_shape(size)
+
+            # shape[2] = float count is only used in vertex shader code
+            texture = texture.reshape(shape[0], shape[1], 4)
+            self._uniforms_texture = Texture2D(texture)
+            self._uniforms_texture.data = texture
+            self._uniforms_texture.interpolation = 'nearest'
+
+        if len(self._programs):
+            for program in self._programs:
+                program.bind(self._vertices_buffer)
+                if self._uniforms_list is not None:
+                    program["uniforms"] = self._uniforms_texture
+                    program["uniforms_shape"] = self._ushape
diff --git a/vispy/visuals/collections/collection.py b/vispy/visuals/collections/collection.py
new file mode 100644
index 0000000..bde7320
--- /dev/null
+++ b/vispy/visuals/collections/collection.py
@@ -0,0 +1,250 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+A collection is a container for several items having the same data
+structure (dtype). Each data type can be declared as local (it specific to a
+vertex), shared (it is shared among an item vertices) or global (it is shared
+by all vertices). It is based on the BaseCollection but offers a more intuitive
+interface.
+"""
+
+import numpy as np
+from ... import gloo
+from . util import fetchcode
+from . base_collection import BaseCollection
+from ..shaders import ModularProgram
+from ...util.event import EventEmitter
+
+
+class Collection(BaseCollection):
+
+    """
+    A collection is a container for several items having the same data
+    structure (dtype). Each data type can be declared as local (it is specific
+    to a vertex), shared (it is shared among item vertices) or global (it is
+    shared by all items). It is based on the BaseCollection but offers a more
+    intuitive interface.
+
+    Parameters
+    ----------
+
+    dtype: list
+        Data individual types as (name, dtype, scope, default)
+
+    itype: np.dtype or None
+        Indices data type
+
+    mode : GL_ENUM
+        GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP,
+        GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN
+
+    vertex: str or tuple of str
+       Vertex shader to use to draw this collection
+
+    fragment:  str or tuple of str
+       Fragment shader to use to draw this collection
+
+    kwargs: str
+        Scope can also be specified using keyword argument,
+        where parameter name must be one of the dtype.
+    """
+
+    _gtypes = {('float32', 1): "float",
+               ('float32', 2): "vec2",
+               ('float32', 3): "vec3",
+               ('float32', 4): "vec4",
+               ('int32', 1): "int",
+               ('int32', 2): "ivec2",
+               ('int32', 3): "ivec3",
+               ('int32', 4): "ivec4"}
+
+    def __init__(self, dtype, itype, mode, vertex, fragment, **kwargs):
+        """
+        """
+
+        self._uniforms = {}
+        self._attributes = {}
+        self._varyings = {}
+        self._mode = mode
+        vtype = []
+        utype = []
+        
+        self.update = EventEmitter(source=self, type='collection_update')
+
+        # Build vtype and utype according to parameters
+        declarations = {"uniforms": "",
+                        "attributes": "",
+                        "varyings": ""}
+        defaults = {}
+        for item in dtype:
+            name, (basetype, count), scope, default = item
+            basetype = np.dtype(basetype).name
+            if scope[0] == "!":
+                scope = scope[1:]
+            else:
+                scope = kwargs.get(name, scope)
+            defaults[name] = default
+            gtype = Collection._gtypes[(basetype, count)]
+            if scope == "local":
+                vtype.append((name, basetype, count))
+                declarations[
+                    "attributes"] += "attribute %s %s;\n" % (gtype, name)
+            elif scope == "shared":
+                utype.append((name, basetype, count))
+                declarations["varyings"] += "varying %s %s;\n" % (gtype, name)
+            else:
+                declarations["uniforms"] += "uniform %s %s;\n" % (gtype, name)
+                self._uniforms[name] = None
+
+        vtype = np.dtype(vtype)
+        itype = np.dtype(itype) if itype else None
+        utype = np.dtype(utype) if utype else None
+
+        BaseCollection.__init__(self, vtype=vtype, utype=utype, itype=itype)
+        self._declarations = declarations
+        self._defaults = defaults
+
+        # Build program (once base collection is built)
+        saved = vertex
+        vertex = ""
+
+        if self.utype is not None:
+            vertex += fetchcode(self.utype) + vertex
+        else:
+            vertex += "void fetch_uniforms(void) { }\n" + vertex
+        vertex += self._declarations["uniforms"]
+        vertex += self._declarations["attributes"]
+        vertex += saved
+
+        self._vertex = vertex
+        self._fragment = fragment
+
+        program = ModularProgram(vertex, fragment)
+        program.changed.connect(self.update)
+        self._programs.append(program)
+
+        # Initialize uniforms
+        for name in self._uniforms.keys():
+            self._uniforms[name] = self._defaults.get(name)
+            program[name] = self._uniforms[name]
+
+    def view(self, transform, viewport=None):
+        """ Return a view on the collection using provided transform """
+
+        return CollectionView(self, transform, viewport)
+
+        # program = gloo.Program(self._vertex, self._fragment)
+        # if "transform" in program.hooks:
+        #     program["transform"] = transform
+        # if "viewport" in program.hooks:
+        #     if viewport is not None:
+        #         program["viewport"] = viewport
+        #     else:
+        #         program["viewport"] = Viewport()
+        # self._programs.append(program)
+        # program.bind(self._vertices_buffer)
+        # for name in self._uniforms.keys():
+        #     program[name] = self._uniforms[name]
+        # #if self._uniforms_list is not None:
+        # #    program["uniforms"] = self._uniforms_texture
+        # #    program["uniforms_shape"] = self._ushape
+
+        # # Piggy backing
+        # def draw():
+        #     if self._need_update:
+        #         self._update()
+        #         program.bind(self._vertices_buffer)
+        #         if self._uniforms_list is not None:
+        #             program["uniforms"] = self._uniforms_texture
+        #             program["uniforms_shape"] = self._ushape
+
+        #     if self._indices_list is not None:
+        #         Program.draw(program, self._mode, self._indices_buffer)
+        #     else:
+        #         Program.draw(program, self._mode)
+
+        # program.draw = draw
+        # return program
+
+    def __getitem__(self, key):
+
+        program = self._programs[0]
+        for name, (storage, _, _) in program._code_variables.items():
+            if name == key and storage == 'uniform':
+                return program[key]
+        return BaseCollection.__getitem__(self, key)
+
+    def __setitem__(self, key, value):
+
+        found = False
+        for program in self._programs:
+            program.build_if_needed()
+            for name, (storage, _, _, _) in program._code_variables.items():
+                if name == key and storage == 'uniform':
+                    found = True
+                    program[key] = value
+        if not found:
+            BaseCollection.__setitem__(self, key, value)
+
+    def draw(self, mode=None):
+        """ Draw collection """
+
+        if self._need_update:
+            self._update()
+
+        program = self._programs[0]
+
+        mode = mode or self._mode
+        if self._indices_list is not None:
+            program.draw(mode, self._indices_buffer)
+        else:
+            program.draw(mode)
+
+
+class CollectionView(object):
+
+    def __init__(self, collection, transform=None, viewport=None):
+
+        vertex = collection._vertex
+        fragment = collection._fragment
+        program = gloo.Program(vertex, fragment)
+
+#        if "transform" in program.hooks and transform is not None:
+#            program["transform"] = transform
+#        if "viewport" in program.hooks and viewport is not None:
+#            program["viewport"] = viewport
+
+        program.bind(collection._vertices_buffer)
+        for name in collection._uniforms.keys():
+            program[name] = collection._uniforms[name]
+
+        collection._programs.append(program)
+        self._program = program
+        self._collection = collection
+
+    def __getitem__(self, key):
+        return self._program[key]
+
+    def __setitem__(self, key, value):
+        self._program[key] = value
+
+    def draw(self):
+
+        program = self._program
+        collection = self._collection
+        mode = collection._mode
+
+        if collection._need_update:
+            collection._update()
+            # self._program.bind(self._vertices_buffer)
+            if collection._uniforms_list is not None:
+                program["uniforms"] = collection._uniforms_texture
+                program["uniforms_shape"] = collection._ushape
+
+        if collection._indices_list is not None:
+            program.draw(mode, collection._indices_buffer)
+        else:
+            program.draw(mode)
diff --git a/vispy/visuals/collections/path_collection.py b/vispy/visuals/collections/path_collection.py
new file mode 100644
index 0000000..73fbd1a
--- /dev/null
+++ b/vispy/visuals/collections/path_collection.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+from . raw_path_collection import RawPathCollection
+from . agg_path_collection import AggPathCollection
+from . agg_fast_path_collection import AggFastPathCollection
+
+
+def PathCollection(mode="agg", *args, **kwargs):
+    """
+    mode: string
+      - "raw"   (speed: fastest, size: small, output: ugly, no dash,
+                 no thickness)
+      - "agg"   (speed: medium, size: medium output: nice, some flaws, no dash)
+      - "agg+"  (speed: slow, size: big, output: perfect, no dash)
+    """
+
+    if mode == "raw":
+        return RawPathCollection(*args, **kwargs)
+    elif mode == "agg+":
+        return AggPathCollection(*args, **kwargs)
+    return AggFastPathCollection(*args, **kwargs)
diff --git a/vispy/visuals/collections/point_collection.py b/vispy/visuals/collections/point_collection.py
new file mode 100644
index 0000000..38da601
--- /dev/null
+++ b/vispy/visuals/collections/point_collection.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+from . raw_point_collection import RawPointCollection
+from . agg_point_collection import AggPointCollection
+
+
+def PointCollection(mode="raw", *args, **kwargs):
+    """
+    mode: string
+      - "raw"  (speed: fastest, size: small,   output: ugly)
+      - "agg"  (speed: fast,    size: small,   output: beautiful)
+    """
+
+    if mode == "raw":
+        return RawPointCollection(*args, **kwargs)
+    return AggPointCollection(*args, **kwargs)
diff --git a/vispy/visuals/collections/polygon_collection.py b/vispy/visuals/collections/polygon_collection.py
new file mode 100644
index 0000000..f802400
--- /dev/null
+++ b/vispy/visuals/collections/polygon_collection.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+""" """
+from . raw_polygon_collection import RawPolygonCollection
+#from . agg_polygon_collection import AggPolygonCollection
+#from . agg_fast_polygon_collection import AggPolygonCollection
+
+
+def PolygonCollection(mode="raw", *args, **kwargs):
+    """
+    mode: string
+      - "raw"   (speed: fastest, size: small, output: ugly, no dash, no
+                 thickness)
+      - "agg"   (speed: medium,  size: medium output: nice, some flaws, no
+                 dash)
+      - "agg+"  (speed: slow, size: big, output: perfect, no dash)
+    """
+
+    # if mode == "raw":
+    return RawPolygonCollection(*args, **kwargs)
+    # elif mode == "agg":
+    #    return AggFastPolygonCollection(*args, **kwargs)
+    # return AggPolygonCollection(*args, **kwargs)
diff --git a/vispy/visuals/collections/raw_path_collection.py b/vispy/visuals/collections/raw_path_collection.py
new file mode 100644
index 0000000..df31e3c
--- /dev/null
+++ b/vispy/visuals/collections/raw_path_collection.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+import numpy as np
+from vispy import glsl
+from . collection import Collection
+from ..transforms import NullTransform
+
+
+class RawPathCollection(Collection):
+
+    """
+    """
+
+    def __init__(self, user_dtype=None, transform=None,
+                 vertex=None, fragment=None, **kwargs):
+        """
+        Initialize the collection.
+
+        Parameters
+        ----------
+
+        user_dtype: list
+            The base dtype can be completed (appended) by the used_dtype. It
+            only make sense if user also provide vertex and/or fragment shaders
+
+        transform : Transform instance
+            Used to define the transform(vec4) function
+
+        vertex: string
+            Vertex shader code
+
+        fragment: string
+            Fragment  shader code
+
+        color : string
+            'local', 'shared' or 'global'
+        """
+
+        base_dtype = [('position', (np.float32, 3), '!local', (0, 0, 0)),
+                      ('id',       (np.float32, 1), '!local', 0),
+                      ('color',    (np.float32, 4), 'local', (0, 0, 0, 1)),
+                      ("linewidth", (np.float32, 1), 'global', 1),
+                      ("viewport", (np.float32, 4), 'global', (0, 0, 512, 512))
+                      ]
+
+        dtype = base_dtype
+        if user_dtype:
+            dtype.extend(user_dtype)
+
+        if vertex is None:
+            vertex = glsl.get('collections/raw-path.vert')
+        if transform is None:
+            transform = NullTransform()
+        self.transform = transform        
+        if fragment is None:
+            fragment = glsl.get('collections/raw-path.frag')
+
+        vertex = transform + vertex
+        Collection.__init__(self, dtype=dtype, itype=None, mode='line_strip',
+                            vertex=vertex, fragment=fragment, **kwargs)
+        self._programs[0].vert['transform'] = self.transform
+
+    def append(self, P, closed=False, itemsize=None, **kwargs):
+        """
+        Append a new set of vertices to the collection.
+
+        For kwargs argument, n is the number of vertices (local) or the number
+        of item (shared)
+
+        Parameters
+        ----------
+
+        P : np.array
+            Vertices positions of the path(s) to be added
+
+        closed: bool
+            Whether path(s) is/are closed
+
+        itemsize: int or None
+            Size of an individual path
+
+        color : list, array or 4-tuple
+           Path color
+        """
+
+        itemsize = itemsize or len(P)
+        itemcount = len(P) / itemsize
+        P = P.reshape(itemcount, itemsize, 3)
+        if closed:
+            V = np.empty((itemcount, itemsize + 3), dtype=self.vtype)
+            # Apply default values on vertices
+            for name in self.vtype.names:
+                if name not in ['collection_index', 'position']:
+                    V[name][1:-2] = kwargs.get(name, self._defaults[name])
+            V["position"][:, 1:-2] = P
+            V["position"][:,  -2] = V["position"][:, 1]
+        else:
+            V = np.empty((itemcount, itemsize + 2), dtype=self.vtype)
+            # Apply default values on vertices
+            for name in self.vtype.names:
+                if name not in ['collection_index', 'position']:
+                    V[name][1:-1] = kwargs.get(name, self._defaults[name])
+            V["position"][:, 1:-1] = P
+        V["id"] = 1
+        V[:, 0] = V[:, 1]
+        V[:, -1] = V[:, -2]
+        V["id"][:, 0] = 0
+        V["id"][:, -1] = 0
+
+        # Uniforms
+        if self.utype:
+            U = np.zeros(itemcount, dtype=self.utype)
+            for name in self.utype.names:
+                if name not in ["__unused__"]:
+                    U[name] = kwargs.get(name, self._defaults[name])
+        else:
+            U = None
+
+        Collection.append(self, vertices=V, uniforms=U,
+                          itemsize=itemsize + 2 + closed)
diff --git a/vispy/visuals/collections/raw_point_collection.py b/vispy/visuals/collections/raw_point_collection.py
new file mode 100644
index 0000000..efb55f2
--- /dev/null
+++ b/vispy/visuals/collections/raw_point_collection.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Raw Point Collection
+
+This collection provides very fast points. Output quality is ugly so it must be
+used at small size only (2/3 pixels). You've been warned.
+"""
+
+import numpy as np
+from vispy import glsl
+from . collection import Collection
+from ..transforms import NullTransform
+
+
+class RawPointCollection(Collection):
+
+    """
+    Raw Point Collection
+
+    This collection provides very fast points. Output quality is ugly so it
+    must be used at small size only (2/3 pixels). You've been warned.
+    """
+
+    def __init__(self, user_dtype=None, transform=None,
+                 vertex=None, fragment=None, **kwargs):
+        """
+        Initialize the collection.
+
+        Parameters
+        ----------
+
+        user_dtype: list
+            The base dtype can be completed (appended) by the used_dtype. It
+            only make sense if user also provide vertex and/or fragment shaders
+
+        transform : Transform instance
+            Used to define the transform(vec4) function
+
+        vertex: string
+            Vertex shader code
+
+        fragment: string
+            Fragment  shader code
+
+        color : string
+            'local', 'shared' or 'global'
+        """
+        base_dtype = [('position', (np.float32, 3), "!local", (0, 0, 0)),
+                      ('size',     (np.float32, 1), "global", 3.0),
+                      ('color',    (np.float32, 4), "global", (0, 0, 0, 1))]
+
+        dtype = base_dtype
+        if user_dtype:
+            dtype.extend(user_dtype)
+
+        if vertex is None:
+            vertex = glsl.get("collections/raw-point.vert")
+        if transform is None:
+            transform = NullTransform()
+        self.transform = transform        
+        if fragment is None:
+            fragment = glsl.get("collections/raw-point.frag")
+
+        Collection.__init__(self, dtype=dtype, itype=None, mode="points",
+                            vertex=vertex, fragment=fragment, **kwargs)
+
+        # Set hooks if necessary
+        program = self._programs[0]
+        program.vert['transform'] = self.transform
+
+    def append(self, P, itemsize=None, **kwargs):
+        """
+        Append a new set of vertices to the collection.
+
+        For kwargs argument, n is the number of vertices (local) or the number
+        of item (shared)
+
+        Parameters
+        ----------
+
+        P : np.array
+            Vertices positions of the points(s) to be added
+
+        itemsize: int or None
+            Size of an individual path
+
+        color : list, array or 4-tuple
+           Path color
+        """
+
+        itemsize = itemsize or 1
+        itemcount = len(P) / itemsize
+
+        V = np.empty(len(P), dtype=self.vtype)
+
+        # Apply default values on vertices
+        for name in self.vtype.names:
+            if name not in ['position', "collection_index"]:
+                V[name] = kwargs.get(name, self._defaults[name])
+        V["position"] = P
+
+        # Uniforms
+        if self.utype:
+            U = np.zeros(itemcount, dtype=self.utype)
+            for name in self.utype.names:
+                if name not in ["__unused__"]:
+                    U[name] = kwargs.get(name, self._defaults[name])
+        else:
+            U = None
+
+        Collection.append(self, vertices=V, uniforms=U, itemsize=itemsize)
diff --git a/vispy/visuals/collections/raw_polygon_collection.py b/vispy/visuals/collections/raw_polygon_collection.py
new file mode 100644
index 0000000..d9fdf7c
--- /dev/null
+++ b/vispy/visuals/collections/raw_polygon_collection.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+import numpy as np
+from vispy import glsl
+from . collection import Collection
+from ..transforms import NullTransform
+from ...geometry import triangulate
+
+
+class RawPolygonCollection(Collection):
+
+    def __init__(self, user_dtype=None, transform=None,
+                 vertex=None, fragment=None, **kwargs):
+
+        base_dtype = [('position', (np.float32, 3), '!local', (0, 0, 0)),
+                      ('color',    (np.float32, 4), 'local',  (0, 0, 0, 1))]
+
+        dtype = base_dtype
+        if user_dtype:
+            dtype.extend(user_dtype)
+
+        if vertex is None:
+            vertex = glsl.get('collections/raw-triangle.vert')
+        if transform is None:
+            transform = NullTransform()
+        self.transform = transform        
+        if fragment is None:
+            fragment = glsl.get('collections/raw-triangle.frag')
+
+        Collection.__init__(self, dtype=dtype, itype=np.uint32,  # 16 for WebGL
+                            mode="triangles",
+                            vertex=vertex, fragment=fragment, **kwargs)
+
+        # Set hooks if necessary
+        program = self._programs[0]
+        program.vert['transform'] = self.transform
+
+    def append(self, points, **kwargs):
+        """
+        Append a new set of vertices to the collection.
+
+        For kwargs argument, n is the number of vertices (local) or the number
+        of item (shared)
+
+        Parameters
+        ----------
+
+        points : np.array
+            Vertices composing the triangles
+
+        color : list, array or 4-tuple
+           Path color
+        """
+
+        vertices, indices = triangulate(points)
+        itemsize = len(vertices)
+        itemcount = 1
+
+        V = np.empty(itemcount * itemsize, dtype=self.vtype)
+        for name in self.vtype.names:
+            if name not in ['collection_index', 'position']:
+                V[name] = kwargs.get(name, self._defaults[name])
+        V["position"] = vertices
+
+        # Uniforms
+        if self.utype:
+            U = np.zeros(itemcount, dtype=self.utype)
+            for name in self.utype.names:
+                if name not in ["__unused__"]:
+                    U[name] = kwargs.get(name, self._defaults[name])
+        else:
+            U = None
+
+        I = np.array(indices).ravel()
+        Collection.append(self, vertices=V, uniforms=U, indices=I,
+                          itemsize=itemsize)
diff --git a/vispy/visuals/collections/raw_segment_collection.py b/vispy/visuals/collections/raw_segment_collection.py
new file mode 100644
index 0000000..c82e97c
--- /dev/null
+++ b/vispy/visuals/collections/raw_segment_collection.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Raw Segment Collection
+
+This collection provides fast raw (& ugly) line segments.
+"""
+import numpy as np
+from vispy import glsl
+from . collection import Collection
+from ..transforms import NullTransform
+
+
+class RawSegmentCollection(Collection):
+
+    """
+    Raw Segment Collection
+
+    This collection provides fast raw (& ugly) line segments.
+    """
+
+    def __init__(self, user_dtype=None, transform=None,
+                 vertex=None, fragment=None, **kwargs):
+        """
+        Initialize the collection.
+
+        Parameters
+        ----------
+
+        user_dtype: list
+            The base dtype can be completed (appended) by the used_dtype. It
+            only make sense if user also provide vertex and/or fragment shaders
+
+        transform : string
+            GLSL Transform code defining the vec4 transform(vec3) function
+
+        vertex: string
+            Vertex shader code
+
+        fragment: string
+            Fragment  shader code
+
+        color : string
+            'local', 'shared' or 'global'
+        """
+
+        base_dtype = [("position", (np.float32, 3), "!local", (0, 0, 0)),
+                      ("color",    (np.float32, 4), "global", (0, 0, 0, 1)),
+                      ("viewport", (np.float32, 4), "global", (0, 0, 512, 512))
+                      ]
+
+        dtype = base_dtype
+        if user_dtype:
+            dtype.extend(user_dtype)
+
+        if vertex is None:
+            vertex = glsl.get('collections/raw-segment.vert')
+        if transform is None:
+            transform = NullTransform()
+        self.transform = transform        
+        if fragment is None:
+            fragment = glsl.get('collections/raw-segment.frag')
+
+        Collection.__init__(self, dtype=dtype, itype=None, mode='lines',
+                            vertex=vertex, fragment=fragment, **kwargs)
+        self._programs[0].vert['transform'] = self.transform
+
+    def append(self, P0, P1, itemsize=None, **kwargs):
+        """
+        Append a new set of segments to the collection.
+
+        For kwargs argument, n is the number of vertices (local) or the number
+        of item (shared)
+
+        Parameters
+        ----------
+
+        P : np.array
+            Vertices positions of the path(s) to be added
+
+        closed: bool
+            Whether path(s) is/are closed
+
+        itemsize: int or None
+            Size of an individual path
+
+        color : list, array or 4-tuple
+           Path color
+        """
+
+        itemsize = itemsize or 1
+        itemcount = len(P0) / itemsize
+
+        V = np.empty(itemcount, dtype=self.vtype)
+
+        # Apply default values on vertices
+        for name in self.vtype.names:
+            if name not in ['collection_index', 'P']:
+                V[name] = kwargs.get(name, self._defaults[name])
+
+        V = np.repeat(V, 2, axis=0)
+        V['P'][0::2] = P0
+        V['P'][1::2] = P1
+
+        # Uniforms
+        if self.utype:
+            U = np.zeros(itemcount, dtype=self.utype)
+            for name in self.utype.names:
+                if name not in ["__unused__"]:
+                    U[name] = kwargs.get(name, self._defaults[name])
+        else:
+            U = None
+
+        Collection.append(self, vertices=V, uniforms=U, itemsize=itemsize)
diff --git a/vispy/visuals/collections/raw_triangle_collection.py b/vispy/visuals/collections/raw_triangle_collection.py
new file mode 100644
index 0000000..42ad5f8
--- /dev/null
+++ b/vispy/visuals/collections/raw_triangle_collection.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+import numpy as np
+from vispy import glsl
+from . collection import Collection
+from ..transforms import NullTransform
+
+
+class RawTriangleCollection(Collection):
+
+    """
+    """
+
+    def __init__(self, user_dtype=None, transform=None,
+                 vertex=None, fragment=None, **kwargs):
+
+        base_dtype = [('position', (np.float32, 3), '!local', (0, 0, 0)),
+                      ('color',    (np.float32, 4), 'local', (0, 0, 0, 1))]
+
+        dtype = base_dtype
+        if user_dtype:
+            dtype.extend(user_dtype)
+
+        if vertex is None:
+            vertex = glsl.get('collections/raw-triangle.vert')
+        if transform is None:
+            transform = NullTransform()
+        self.transform = transform        
+        if fragment is None:
+            fragment = glsl.get('collections/raw-triangle.frag')
+
+        Collection.__init__(self, dtype=dtype, itype=np.uint32,
+                            mode="triangles",
+                            vertex=vertex, fragment=fragment, **kwargs)
+        self._programs[0].vert['transform'] = self.transform
+
+    def append(self, points, indices, **kwargs):
+        """
+        Append a new set of vertices to the collection.
+
+        For kwargs argument, n is the number of vertices (local) or the number
+        of item (shared)
+
+        Parameters
+        ----------
+
+        points : np.array
+            Vertices composing the triangles
+
+        indices : np.array
+            Indices describing triangles
+
+        color : list, array or 4-tuple
+           Path color
+        """
+
+        itemsize = len(points)
+        itemcount = 1
+
+        V = np.empty(itemcount * itemsize, dtype=self.vtype)
+        for name in self.vtype.names:
+            if name not in ['collection_index', 'position']:
+                V[name] = kwargs.get(name, self._defaults[name])
+        V["position"] = points
+
+        # Uniforms
+        if self.utype:
+            U = np.zeros(itemcount, dtype=self.utype)
+            for name in self.utype.names:
+                if name not in ["__unused__"]:
+                    U[name] = kwargs.get(name, self._defaults[name])
+        else:
+            U = None
+
+        I = np.array(indices).ravel()
+
+        Collection.append(self, vertices=V, uniforms=U, indices=I,
+                          itemsize=itemsize)
diff --git a/vispy/visuals/collections/segment_collection.py b/vispy/visuals/collections/segment_collection.py
new file mode 100644
index 0000000..2fb9ff4
--- /dev/null
+++ b/vispy/visuals/collections/segment_collection.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+from . raw_segment_collection import RawSegmentCollection
+from . agg_segment_collection import AggSegmentCollection
+
+
+def SegmentCollection(mode="agg-fast", *args, **kwargs):
+    """
+    mode: string
+      - "raw" (speed: fastest, size: small, output: ugly, no dash,
+               no thickness)
+      - "agg" (speed: slower, size: medium, output: perfect, no dash)
+    """
+
+    if mode == "raw":
+        return RawSegmentCollection(*args, **kwargs)
+    return AggSegmentCollection(*args, **kwargs)
diff --git a/vispy/visuals/collections/triangle_collection.py b/vispy/visuals/collections/triangle_collection.py
new file mode 100644
index 0000000..a9695ee
--- /dev/null
+++ b/vispy/visuals/collections/triangle_collection.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2014, Nicolas P. Rougier
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+from . raw_triangle_collection import RawTriangleCollection
+
+
+def TriangleCollection(mode="raw", *args, **kwargs):
+    """
+    mode: string
+      - "raw"  (speed: fastest, size: small,   output: ugly)
+      - "agg"  (speed: fast,    size: small,   output: beautiful)
+    """
+
+    return RawTriangleCollection(*args, **kwargs)
diff --git a/vispy/visuals/collections/util.py b/vispy/visuals/collections/util.py
new file mode 100644
index 0000000..426ec6e
--- /dev/null
+++ b/vispy/visuals/collections/util.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2013, Nicolas P. Rougier. All rights reserved.
+# Distributed under the terms of the new BSD License.
+# -----------------------------------------------------------------------------
+
+import numpy as np
+from functools import reduce
+from operator import mul
+
+
+def dtype_reduce(dtype, level=0, depth=0):
+    """
+    Try to reduce dtype up to a given level when it is possible
+
+    dtype =  [ ('vertex',  [('x', 'f4'), ('y', 'f4'), ('z', 'f4')]),
+               ('normal',  [('x', 'f4'), ('y', 'f4'), ('z', 'f4')]),
+               ('color',   [('r', 'f4'), ('g', 'f4'), ('b', 'f4'),
+                            ('a', 'f4')])]
+
+    level 0: ['color,vertex,normal,', 10, 'float32']
+    level 1: [['color', 4, 'float32']
+              ['normal', 3, 'float32']
+              ['vertex', 3, 'float32']]
+    """
+    dtype = np.dtype(dtype)
+    fields = dtype.fields
+
+    # No fields
+    if fields is None:
+        if len(dtype.shape):
+            count = reduce(mul, dtype.shape)
+        else:
+            count = 1
+        # size = dtype.itemsize / count
+        if dtype.subdtype:
+            name = str(dtype.subdtype[0])
+        else:
+            name = str(dtype)
+        return ['', count, name]
+    else:
+        items = []
+        name = ''
+        # Get reduced fields
+        for key, value in fields.items():
+            l = dtype_reduce(value[0], level, depth + 1)
+            if type(l[0]) is str:
+                items.append([key, l[1], l[2]])
+            else:
+                items.append(l)
+            name += key + ','
+
+        # Check if we can reduce item list
+        ctype = None
+        count = 0
+        for i, item in enumerate(items):
+            # One item is a list, we cannot reduce
+            if type(item[0]) is not str:
+                return items
+            else:
+                if i == 0:
+                    ctype = item[2]
+                    count += item[1]
+                else:
+                    if item[2] != ctype:
+                        return items
+                    count += item[1]
+        if depth >= level:
+            return [name, count, ctype]
+        else:
+            return items
+
+
+def fetchcode(utype, prefix=""):
+    """
+    Generate the GLSL code needed to retrieve fake uniform values from a
+    texture.
+
+    uniforms : sampler2D
+        Texture to fetch uniforms from
+
+    uniforms_shape: vec3
+        Size of texture (width,height,count) where count is the number of float
+        to be fetched.
+
+    collection_index: float
+        Attribute giving the index of the uniforms to be fetched. This index
+       relates to the index in the uniform array from python side.
+    """
+
+    utype = np.dtype(utype)
+    _utype = dtype_reduce(utype, level=1)
+
+    header = """
+uniform   sampler2D uniforms;
+uniform   vec3      uniforms_shape;
+attribute float     collection_index;
+
+"""
+
+    # Header generation (easy)
+    types = {1: 'float', 2: 'vec2 ', 3: 'vec3 ',
+             4: 'vec4 ', 9: 'mat3 ', 16: 'mat4 '}
+    for name, count, _ in _utype:
+        if name != '__unused__':
+            header += "varying %s %s%s;\n" % (types[count], prefix, name)
+
+    # Body generation (not so easy)
+    body = """\nvoid fetch_uniforms() {
+    float rows   = uniforms_shape.x;
+    float cols   = uniforms_shape.y;
+    float count  = uniforms_shape.z;
+    float index  = collection_index;
+    int index_x  = int(mod(index, (floor(cols/(count/4.0))))) * int(count/4.0);
+    int index_y  = int(floor(index / (floor(cols/(count/4.0)))));
+    float size_x = cols - 1.0;
+    float size_y = rows - 1.0;
+    float ty     = 0.0;
+    if (size_y > 0.0)
+        ty = float(index_y)/size_y;
+    int i = index_x;
+    vec4 _uniform;\n"""
+
+    _utype = dict([(name, count) for name, count, _ in _utype])
+    store = 0
+    # Be very careful with utype name order (_utype.keys is wrong)
+    for name in utype.names:
+        if name == '__unused__':
+            continue
+        count, shift = _utype[name], 0
+        size = count
+        while count:
+            if store == 0:
+                body += "\n    _uniform = texture2D(uniforms, vec2(float(i++)/size_x,ty));\n"  # noqa
+                store = 4
+            if store == 4:
+                a = "xyzw"
+            elif store == 3:
+                a = "yzw"
+            elif store == 2:
+                a = "zw"
+            elif store == 1:
+                a = "w"
+            if shift == 0:
+                b = "xyzw"
+            elif shift == 1:
+                b = "yzw"
+            elif shift == 2:
+                b = "zw"
+            elif shift == 3:
+                b = "w"
+            i = min(min(len(b), count), len(a))
+            if size > 1:
+                body += "    %s%s.%s = _uniform.%s;\n" % (prefix, name, b[:i], a[:i])  # noqa
+            else:
+                body += "    %s%s = _uniform.%s;\n" % (prefix, name, a[:i])
+            count -= i
+            shift += i
+            store -= i
+
+    body += """}\n\n"""
+    return header + body
diff --git a/vispy/scene/components/__init__.py b/vispy/visuals/components/__init__.py
similarity index 80%
rename from vispy/scene/components/__init__.py
rename to vispy/visuals/components/__init__.py
index a613f3f..73abbbf 100644
--- a/vispy/scene/components/__init__.py
+++ b/vispy/visuals/components/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from .color import UniformColorComponent, VertexColorComponent  # noqa
@@ -9,3 +9,7 @@ from .normal import VertexNormalComponent  # noqa
 from .texture import (TextureComponent, VertexTextureCoordinateComponent,  # noqa
                       TextureCoordinateComponent)  # noqa
 from .vertex import XYPosComponent, XYZPosComponent, HeightFieldComponent  # noqa
+
+
+from .clipper import Clipper  # noqa
+from .color2 import Alpha, ColorFilter  # noqa
diff --git a/vispy/visuals/components/clipper.py b/vispy/visuals/components/clipper.py
new file mode 100644
index 0000000..64f3b54
--- /dev/null
+++ b/vispy/visuals/components/clipper.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+import weakref
+from ..shaders import Function
+from ..transforms import NullTransform
+from ...geometry import Rect
+
+
+clip_frag = """
+void clip() {
+    vec4 pos = $fb_to_clip(gl_FragCoord);
+    if( pos.x < $view.x || pos.x > $view.y || 
+        pos.y < $view.z || pos.y > $view.w ) {
+        discard;
+    }
+}
+"""
+
+
+class Clipper(object):
+    """Clips visual output to a rectangular region.
+    """
+    def __init__(self, bounds=(0, 0, 1, 1), transform=None):
+        self.clip_shader = Function(clip_frag)
+        self.clip_expr = self.clip_shader()
+        self.bounds = bounds  # (x, y, w, h)
+        if transform is None:
+            transform = NullTransform()
+        self.set_transform(transform)
+    
+    @property
+    def bounds(self):
+        return self._bounds
+    
+    @bounds.setter
+    def bounds(self, b):
+        self._bounds = Rect(b).normalized()
+        b = self._bounds
+        self.clip_shader['view'] = (b.left, b.right, b.bottom, b.top)
+        
+    def _attach(self, visual):
+        self._visual = weakref.ref(visual)
+        try:
+            hook = visual._get_hook('frag', 'pre')
+        except KeyError:
+            raise NotImplementedError("Visual %s does not support clipping" %
+                                      visual)
+        hook.add(self.clip_expr)
+
+    def set_transform(self, tr):
+        self.clip_shader['fb_to_clip'] = tr
diff --git a/vispy/scene/components/color.py b/vispy/visuals/components/color.py
similarity index 97%
rename from vispy/scene/components/color.py
rename to vispy/visuals/components/color.py
index e2d6e1e..ff66f55 100644
--- a/vispy/scene/components/color.py
+++ b/vispy/visuals/components/color.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
diff --git a/vispy/visuals/components/color2.py b/vispy/visuals/components/color2.py
new file mode 100644
index 0000000..18b7a75
--- /dev/null
+++ b/vispy/visuals/components/color2.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+import weakref
+
+from ..shaders import Function
+
+# To replace color.py soon..
+
+
+class Alpha(object):
+    def __init__(self, alpha=1.0):
+        self.shader = Function("""
+            void apply_alpha() {
+                gl_FragColor.a = gl_FragColor.a * $alpha;
+            }
+        """)
+        self.alpha = alpha
+    
+    @property
+    def alpha(self):
+        return self._alpha
+    
+    @alpha.setter
+    def alpha(self, a):
+        self._alpha = a
+        self.shader['alpha'] = a
+        
+    def _attach(self, visual):
+        self._visual = weakref.ref(visual)
+        hook = visual._get_hook('frag', 'post')
+        hook.add(self.shader())
+
+
+class ColorFilter(object):
+    def __init__(self, filter=(1, 1, 1, 1)):
+        self.shader = Function("""
+            void apply_color_filter() {
+                gl_FragColor = gl_FragColor * $filter;
+            }
+        """)
+        self.filter = filter
+    
+    @property
+    def filter(self):
+        return self._filter
+    
+    @filter.setter
+    def filter(self, f):
+        self._filter = tuple(f)
+        self.shader['filter'] = self._filter
+        
+    def _attach(self, visual):
+        self._visual = visual
+        hook = self._visual._get_hook('frag', 'post')
+        hook.add(self.shader())
diff --git a/vispy/scene/components/component.py b/vispy/visuals/components/component.py
similarity index 100%
rename from vispy/scene/components/component.py
rename to vispy/visuals/components/component.py
diff --git a/vispy/scene/components/material.py b/vispy/visuals/components/material.py
similarity index 98%
rename from vispy/scene/components/material.py
rename to vispy/visuals/components/material.py
index 0b67aa0..f88ee47 100644
--- a/vispy/scene/components/material.py
+++ b/vispy/visuals/components/material.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
diff --git a/vispy/scene/components/normal.py b/vispy/visuals/components/normal.py
similarity index 94%
rename from vispy/scene/components/normal.py
rename to vispy/visuals/components/normal.py
index 14c61be..119960a 100644
--- a/vispy/scene/components/normal.py
+++ b/vispy/visuals/components/normal.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -60,12 +60,12 @@ class VertexNormalComponent(VisualComponent):
             else:
                 index = None
             if self.smooth:
-                norm = self._meshdata.vertex_normals(indexed=index)
+                norm = self._meshdata.get_vertex_normals(indexed=index)
             else:
                 if index != 'faces':
                     raise Exception("Not possible to draw faceted mesh without"
                                     "pre-indexing.")
-                norm = self._meshdata.face_normals(indexed=index)
+                norm = self._meshdata.get_face_normals(indexed=index)
             self._vbo = gloo.VertexBuffer(norm)
             self._vbo_mode = mode
         return self._vbo
diff --git a/vispy/scene/components/texture.py b/vispy/visuals/components/texture.py
similarity index 97%
rename from vispy/scene/components/texture.py
rename to vispy/visuals/components/texture.py
index f1606be..adb8c6f 100644
--- a/vispy/scene/components/texture.py
+++ b/vispy/visuals/components/texture.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
@@ -93,7 +93,7 @@ class VertexTextureCoordinateComponent(VisualComponent):
 
     def activate(self, program, mode):
         ff = self.coord_shader()
-        ff['map_local_to_tex'] = self.transform.shader_map()
+        ff['map_local_to_tex'] = self.transform
         self._funcs['vert_post_hook']['local_pos'] = \
             self.visual._program.vert['local_pos']
 
diff --git a/vispy/scene/components/vertex.py b/vispy/visuals/components/vertex.py
similarity index 99%
rename from vispy/scene/components/vertex.py
rename to vispy/visuals/components/vertex.py
index baa1ed7..965c75c 100644
--- a/vispy/scene/components/vertex.py
+++ b/vispy/visuals/components/vertex.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
diff --git a/vispy/visuals/cube.py b/vispy/visuals/cube.py
new file mode 100644
index 0000000..9152927
--- /dev/null
+++ b/vispy/visuals/cube.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+from ..geometry import create_cube
+from ..gloo import set_state
+from .mesh import MeshVisual
+
+
+class CubeVisual(MeshVisual):
+    """Visual that displays a cube or cuboid
+
+    Parameters
+    ----------
+    size : float or tuple
+        The size of the cuboid. A float gives a cube, whereas tuples may
+        specify the size of each axis (x, y, z) independently.
+    vertex_colors : ndarray
+        Same as for `MeshVisual` class. See `create_cube` for vertex ordering.
+    face_colors : ndarray
+        Same as for `MeshVisual` class. See `create_cube` for vertex ordering.
+    color : Color
+        The `Color` to use when drawing the cube faces.
+    edge_color : tuple or Color
+        The `Color` to use when drawing the cube edges. If `None`, then no
+        cube edges are drawn.
+    """
+    def __init__(self, size=1.0, vertex_colors=None, face_colors=None,
+                 color=(0.5, 0.5, 1, 1), edge_color=None):
+        vertices, filled_indices, outline_indices = create_cube()
+        vertices['position'] *= size
+
+        MeshVisual.__init__(self, vertices['position'], filled_indices,
+                            vertex_colors, face_colors, color)
+        if edge_color:
+            self._outline = MeshVisual(vertices['position'], outline_indices,
+                                       color=edge_color, mode='lines')
+        else:
+            self._outline = None
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        MeshVisual.draw(self, transforms)
+        if self._outline:
+            set_state(polygon_offset=(1, 1), polygon_offset_fill=True)
+            self._outline.draw(transforms)
diff --git a/vispy/scene/visuals/ellipse.py b/vispy/visuals/ellipse.py
similarity index 73%
rename from vispy/scene/visuals/ellipse.py
rename to vispy/visuals/ellipse.py
index a589e9f..ab1d478 100644
--- a/vispy/scene/visuals/ellipse.py
+++ b/vispy/visuals/ellipse.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 
@@ -10,11 +10,11 @@ Simple ellipse visual based on PolygonVisual
 from __future__ import division
 
 import numpy as np
-from ...color import Color
-from .polygon import Polygon, Mesh, Line
+from ..color import Color
+from .polygon import PolygonVisual
 
 
-class Ellipse(Polygon):
+class EllipseVisual(PolygonVisual):
     """
     Displays a 2D ellipse
 
@@ -22,6 +22,10 @@ class Ellipse(Polygon):
     ----------
     pos : array
         Center of the ellipse
+    color : instance of Color
+        The face color to use.
+    border_color : instance of Color
+        The border color to use.
     radius : float | tuple
         Radius or radii of the ellipse
         Defaults to  (0.1, 0.1)
@@ -37,8 +41,9 @@ class Ellipse(Polygon):
     """
     def __init__(self, pos=None, color='black', border_color=None,
                  radius=(0.1, 0.1), start_angle=0., span_angle=360.,
-                 num_segments=100, **kwds):
-        super(Ellipse, self).__init__()
+                 num_segments=100, **kwargs):
+        super(EllipseVisual, self).__init__()
+        self.mesh.mode = 'triangle_fan'
         self._vertices = None
         self._pos = pos
         self._color = Color(color)
@@ -111,19 +116,24 @@ class Ellipse(Polygon):
     @num_segments.setter
     def num_segments(self, num_segments):
         if num_segments < 1:
-            raise ValueError('Ellipse must consist of more than 1 segment')
+            raise ValueError('EllipseVisual must consist of more than 1 '
+                             'segment')
         self._num_segments = num_segments
         self._update()
 
     def _update(self):
-        if self._pos is not None:
-            self._generate_vertices(pos=self._pos, radius=self._radius,
-                                    start_angle=self._start_angle,
-                                    span_angle=self._span_angle,
-                                    num_segments=self._num_segments)
-            self.mesh = Mesh(vertices=self._vertices, color=self._color.rgba,
-                             mode='triangle_fan')
-            if not self._border_color.is_blank():
-                self.border = Line(pos=self._vertices[1:],
-                                   color=self._border_color.rgba)
-        #self.update()
+        if self._pos is None:
+            return
+        
+        self._generate_vertices(pos=self._pos, radius=self._radius,
+                                start_angle=self._start_angle,
+                                span_angle=self._span_angle,
+                                num_segments=self._num_segments)
+        if not self._color.is_blank:
+            self.mesh.set_data(vertices=self._vertices,
+                               color=self._color.rgba)
+        if not self._border_color.is_blank:
+            self.border.set_data(pos=self._vertices[1:],
+                                 color=self._border_color.rgba)
+        
+        self.update()
diff --git a/vispy/visuals/glsl/__init__.py b/vispy/visuals/glsl/__init__.py
new file mode 100644
index 0000000..177f3be
--- /dev/null
+++ b/vispy/visuals/glsl/__init__.py
@@ -0,0 +1 @@
+"""Repository of common GLSL functions."""
diff --git a/vispy/visuals/glsl/antialiasing.py b/vispy/visuals/glsl/antialiasing.py
new file mode 100644
index 0000000..db26ff3
--- /dev/null
+++ b/vispy/visuals/glsl/antialiasing.py
@@ -0,0 +1,153 @@
+# Copyright (c) 2014, Nicolas P. Rougier. All Rights Reserved.
+# Distributed under the (new) BSD License.
+
+"""Antialiasing GLSL functions."""
+
+
+# -----------------------------------------------------------------------------
+# Stroke
+# -----------------------------------------------------------------------------
+
+"""Compute antialiased fragment color for a stroke line.
+
+Inputs
+------
+
+distance (float): Signed distance to border (in pixels).
+
+
+Template variables
+------------------
+
+linewidth (float): Stroke line width (in pixels).
+
+antialias (float): Stroke antialiased area (in pixels).
+
+stroke (vec4): Stroke color.
+
+
+Outputs
+-------
+color (vec4): The final color.
+
+
+"""
+ANTIALIAS_STROKE = """
+vec4 stroke(float distance)
+{
+    vec4 frag_color;
+    float t = $linewidth/2.0 - $antialias;
+    float signed_distance = distance;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/$antialias;
+    alpha = exp(-alpha*alpha);
+
+    if( border_distance < 0.0 )
+        frag_color = $stroke;
+    else
+        frag_color = vec4($stroke.rgb, $stroke.a * alpha);
+
+    return frag_color;
+}
+"""
+
+
+# -----------------------------------------------------------------------------
+# Stroke
+# -----------------------------------------------------------------------------
+
+"""Compute antialiased fragment color for an outlined shape.
+
+Inputs
+------
+
+distance (float): Signed distance to border (in pixels).
+
+
+Template variables
+------------------
+
+linewidth (float): Stroke line width (in pixels).
+
+antialias (float): Stroke antialiased area (in pixels).
+
+stroke (vec4): Stroke color.
+
+fill (vec4): Fill color.
+
+
+Outputs
+-------
+color (vec4): The final color.
+
+
+"""
+ANTIALIAS_OUTLINE = """
+vec4 outline(float distance)
+{
+    vec4 frag_color;
+    float t = $linewidth/2.0 - $antialias;
+    float signed_distance = distance;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/$antialias;
+    alpha = exp(-alpha*alpha);
+
+    if( border_distance < 0.0 )
+        frag_color = $stroke;
+    else if( signed_distance < 0.0 )
+        frag_color = mix($fill, $stroke, sqrt(alpha));
+    else
+        frag_color = vec4($stroke.rgb, $stroke.a * alpha);
+    return frag_color;
+}
+"""
+
+
+# -----------------------------------------------------------------------------
+# Filled
+# -----------------------------------------------------------------------------
+
+"""Compute antialiased fragment color for a filled shape.
+
+Inputs
+------
+
+distance (float): Signed distance to border (in pixels).
+
+
+Template variables
+------------------
+
+linewidth (float): Stroke line width (in pixels).
+
+antialias (float): Stroke antialiased area (in pixels).
+
+fill (vec4): Fill color.
+
+
+Outputs
+-------
+color (vec4): The final color.
+
+
+"""
+ANTIALIAS_FILLED = """
+vec4 filled(float distance)
+{
+    vec4 frag_color;
+    float t = $linewidth/2.0 - $antialias;
+    float signed_distance = distance;
+    float border_distance = abs(signed_distance) - t;
+    float alpha = border_distance/$antialias;
+    alpha = exp(-alpha*alpha);
+
+    if( border_distance < 0.0 )
+        frag_color = $fill;
+    else if( signed_distance < 0.0 )
+        frag_color = $fill;
+    else
+        frag_color = vec4($fill.rgb, alpha * $fill.a);
+
+    return frag_color;
+}
+"""
diff --git a/vispy/visuals/glsl/color.py b/vispy/visuals/glsl/color.py
new file mode 100644
index 0000000..90689ae
--- /dev/null
+++ b/vispy/visuals/glsl/color.py
@@ -0,0 +1,70 @@
+"""Color-related GLSL functions."""
+
+
+# -----------------------------------------------------------------------------
+# Colormaps
+# -----------------------------------------------------------------------------
+
+"""Texture lookup for a discrete color map stored in a 1*ncolors 2D texture.
+
+The `get_color()` function returns a RGB color from an index integer
+referring to the colormap.
+
+
+Inputs
+------
+
+index (int): The color index.
+
+
+Template variables
+------------------
+
+$ncolors (int): The number of colors in the colormap.
+
+$colormap (2D texture sampler): The sampler for the 2D 1*ncolors colormap
+    texture.
+
+
+Outputs
+-------
+
+color (vec3): The color.
+
+"""
+COLORMAP_TEXTURE = """
+vec3 get_color(int index) {
+    float x = (float(index) + .5) / float($ncolors);
+    return texture2D($colormap, vec2(x, .5)).rgb;
+}
+"""
+
+
+# -----------------------------------------------------------------------------
+# Color space transformations
+# -----------------------------------------------------------------------------
+
+# From http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
+# TODO: unit tests
+HSV_TO_RGB = """
+vec3 hsv_to_rgb(vec3 c)
+{
+    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+"""
+
+
+RGB_TO_HSV = """
+vec3 rgb_to_hsv(vec3 c)
+{
+    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+
+    float d = q.x - min(q.w, q.y);
+    float e = 1.0e-10;
+    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
+}
+"""
diff --git a/vispy/scene/visuals/gridlines.py b/vispy/visuals/gridlines.py
similarity index 65%
rename from vispy/scene/visuals/gridlines.py
rename to vispy/visuals/gridlines.py
index e437ab0..dd76471 100644
--- a/vispy/scene/visuals/gridlines.py
+++ b/vispy/visuals/gridlines.py
@@ -1,15 +1,15 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
 
 import numpy as np
 
-from ... import gloo
+from .. import gloo
 from .visual import Visual
-from ..shaders import ModularProgram
-
+from .shaders import ModularProgram
+from .transforms import TransformCache
 
 VERT = """
 attribute vec2 pos;
@@ -41,7 +41,7 @@ void main() {
 
     float max_alpha = 0.6;
     float x_alpha = 0.0;
-    
+
     if (mod(local_pos.x, 1000 * sx) < px.x) {
         x_alpha = clamp(1 * sx/px.x, 0, max_alpha);
     }
@@ -72,33 +72,50 @@ void main() {
 """
 
 
-class GridLines(Visual):
-    """
+class GridLinesVisual(Visual):
+    """ Displays regularly spaced grid lines in any coordinate system and at
+    any scale.
+
+    Parameters
+    ----------
+    scale : tuple
+        The scale to use.
     """
-    def __init__(self, scale=(1, 1), **kwds):
-        super(Visual, self).__init__(**kwds)
+    def __init__(self, scale=(1, 1), **kwargs):
+        super(GridLinesVisual, self).__init__(**kwargs)
         self._program = ModularProgram(VERT, FRAG)
         self._vbo = None
         self._scale = scale
+        self._tr_cache = TransformCache()
+        self.set_gl_state('additive', cull_face=False)
 
     def _buffer(self):
         if self._vbo is None:
             # quad covers entire view; frag. shader will deal with image shape
-            quad = np.array([[-1, -1, 0], [1, -1, 0], [1, 1, 0],
-                             [-1, -1, 0], [1, 1, 0], [-1, 1, 0]],
+            quad = np.array([[-1, -1], [1, -1], [1, 1],
+                             [-1, -1], [1, 1], [-1, 1]],
                             dtype=np.float32)
             self._vbo = gloo.VertexBuffer(quad)
         return self._vbo
 
-    def draw(self, event):
-        gloo.set_state('additive', cull_face='front_and_back')
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        Visual.draw(self, transforms)
 
-        doc_to_ndc = event.entity_transform(map_from=event.document_cs,
-                                            map_to=event.render_cs)
-        local_to_doc = event.document_transform()
+        doc_to_ndc = self._tr_cache.get([transforms.framebuffer_to_render, 
+                                         transforms.document_to_framebuffer])
+        self._tr_cache.roll()
+        local_to_doc = transforms.visual_to_document
 
-        self._program.frag['map_nd_to_doc'] = doc_to_ndc.shader_imap()
-        self._program.frag['map_doc_to_local'] = local_to_doc.shader_imap()
+        self._program.frag['map_nd_to_doc'] = doc_to_ndc.inverse
+        self._program.frag['map_doc_to_local'] = local_to_doc.inverse
+        
         self._program.prepare()
         self._program['pos'] = self._buffer()
         self._program['scale'] = self._scale
diff --git a/vispy/visuals/histogram.py b/vispy/visuals/histogram.py
new file mode 100644
index 0000000..cfa7800
--- /dev/null
+++ b/vispy/visuals/histogram.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+import numpy as np
+
+from .mesh import MeshVisual
+from ..ext.six import string_types
+
+
+class HistogramVisual(MeshVisual):
+    """Visual that calculates and displays a histogram of data
+
+    Parameters
+    ----------
+    data : array-like
+        Data to histogram. Currently only 1D data is supported.
+    bins : int | array-like
+        Number of bins, or bin edges.
+    color : instance of Color
+        Color of the histogram.
+    orientation : {'h', 'v'}
+        Orientation of the histogram.
+    """
+    def __init__(self, data, bins=10, color='w', orientation='h'):
+        #   4-5
+        #   | |
+        # 1-2/7-8
+        # |/| | |
+        # 0-3-6-9
+        data = np.asarray(data)
+        if data.ndim != 1:
+            raise ValueError('Only 1D data currently supported')
+        if not isinstance(orientation, string_types) or \
+                orientation not in ('h', 'v'):
+            raise ValueError('orientation must be "h" or "v", not %s'
+                             % (orientation,))
+        X, Y = (0, 1) if orientation == 'h' else (1, 0)
+
+        # do the histogramming
+        data, bin_edges = np.histogram(data, bins)
+        # construct our vertices
+        rr = np.zeros((3 * len(bin_edges) - 2, 3), np.float32)
+        rr[:, X] = np.repeat(bin_edges, 3)[1:-1]
+        rr[1::3, Y] = data
+        rr[2::3, Y] = data
+        bin_edges.astype(np.float32)
+        # and now our tris
+        tris = np.zeros((2 * len(bin_edges) - 2, 3), np.uint32)
+        offsets = 3 * np.arange(len(bin_edges) - 1,
+                                dtype=np.uint32)[:, np.newaxis]
+        tri_1 = np.array([0, 2, 1])
+        tri_2 = np.array([2, 0, 3])
+        tris[::2] = tri_1 + offsets
+        tris[1::2] = tri_2 + offsets
+        MeshVisual.__init__(self, rr, tris, color=color)
diff --git a/vispy/visuals/image.py b/vispy/visuals/image.py
new file mode 100644
index 0000000..fe06346
--- /dev/null
+++ b/vispy/visuals/image.py
@@ -0,0 +1,306 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division
+
+import numpy as np
+
+from ..gloo import set_state, Texture2D
+from ..color import get_colormap
+from .shaders import ModularProgram, Function, FunctionChain
+from .transforms import NullTransform
+from .visual import Visual
+from ..ext.six import string_types
+
+
+VERT_SHADER = """
+attribute vec2 a_position;
+attribute vec2 a_texcoord;
+varying vec2 v_texcoord;
+
+void main() {
+    v_texcoord = a_texcoord;
+    gl_Position = $transform(vec4(a_position, 0., 1.));
+}
+"""
+
+FRAG_SHADER = """
+uniform sampler2D u_texture;
+varying vec2 v_texcoord;
+
+void main()
+{
+    vec2 texcoord = $map_uv_to_tex(vec4(v_texcoord, 0, 1)).xy;
+    if(texcoord.x < 0.0 || texcoord.x > 1.0 ||
+       texcoord.y < 0.0 || texcoord.y > 1.0) {
+        discard;
+    }
+    gl_FragColor = $color_transform(texture2D(u_texture, texcoord));
+}
+"""  # noqa
+
+_null_color_transform = 'vec4 pass(vec4 color) { return color; }'
+_c2l = 'float cmap(vec4 color) { return (color.r + color.g + color.b) / 3.; }'
+
+
+class ImageVisual(Visual):
+    """Visual subclass displaying an image.
+
+    Parameters
+    ----------
+    data : ndarray
+        ImageVisual data. Can be shape (M, N), (M, N, 3), or (M, N, 4).
+    method : str
+        Selects method of rendering image in case of non-linear transforms.
+        Each method produces similar results, but may trade efficiency
+        and accuracy. If the transform is linear, this parameter is ignored
+        and a single quad is drawn around the area of the image.
+
+            * 'auto': Automatically select 'impostor' if the image is drawn
+              with a nonlinear transform; otherwise select 'subdivide'.
+            * 'subdivide': ImageVisual is represented as a grid of triangles
+              with texture coordinates linearly mapped.
+            * 'impostor': ImageVisual is represented as a quad covering the
+              entire view, with texture coordinates determined by the
+              transform. This produces the best transformation results, but may
+              be slow.
+
+    grid: tuple (rows, cols)
+        If method='subdivide', this tuple determines the number of rows and
+        columns in the image grid.
+    cmap : str | ColorMap
+        Colormap to use for luminance images.
+    clim : str | tuple
+        Limits to use for the colormap. Can be 'auto' to auto-set bounds to
+        the min and max of the data.
+    **kwargs : dict
+        Keyword arguments to pass to `Visual`.
+
+    Notes
+    -----
+    The colormap functionality through ``cmap`` and ``clim`` are only used
+    if the data are 2D.
+    """
+    def __init__(self, data=None, method='auto', grid=(10, 10),
+                 cmap='cubehelix', clim='auto', **kwargs):
+        super(ImageVisual, self).__init__(**kwargs)
+        self._program = ModularProgram(VERT_SHADER, FRAG_SHADER)
+        self.clim = clim
+        self.cmap = cmap
+
+        self._data = None
+
+        self._texture = None
+        self._interpolation = 'nearest'
+        if data is not None:
+            self.set_data(data)
+
+        self._method = method
+        self._method_used = None
+        self._grid = grid
+        self._need_vertex_update = True
+
+    def set_data(self, image):
+        """Set the data
+
+        Parameters
+        ----------
+        image : array-like
+            The image data.
+        """
+        data = np.asarray(image)
+        if self._data is None or self._data.shape != data.shape:
+            self._need_vertex_update = True
+        self._data = data
+        self._texture = None
+
+    @property
+    def clim(self):
+        return (self._clim if isinstance(self._clim, string_types) else
+                tuple(self._clim))
+
+    @clim.setter
+    def clim(self, clim):
+        if isinstance(clim, string_types):
+            if clim != 'auto':
+                raise ValueError('clim must be "auto" if a string')
+        else:
+            clim = np.array(clim, float)
+            if clim.shape != (2,):
+                raise ValueError('clim must have two elements')
+        self._clim = clim
+        self._need_vertex_update = True
+        self.update()
+
+    @property
+    def cmap(self):
+        return self._cmap
+
+    @cmap.setter
+    def cmap(self, cmap):
+        self._cmap = get_colormap(cmap)
+        self.update()
+
+    @property
+    def method(self):
+        return self._method
+    
+    @method.setter
+    def method(self, m):
+        if self._method != m:
+            self._method = m
+            self._need_vertex_update = True
+            self.update()
+
+    @property
+    def size(self):
+        return self._data.shape[:2][::-1]
+
+    def _build_vertex_data(self, transforms):
+        method = self._method
+        grid = self._grid
+        if method == 'auto':
+            if transforms.get_full_transform().Linear:
+                method = 'subdivide'
+                grid = (1, 1)
+            else:
+                method = 'impostor'
+        self._method_used = method
+
+        # TODO: subdivision and impostor modes should be handled by new
+        # components?
+        if method == 'subdivide':
+            # quads cover area of image as closely as possible
+            w = 1.0 / grid[1]
+            h = 1.0 / grid[0]
+
+            quad = np.array([[0, 0, 0], [w, 0, 0], [w, h, 0],
+                             [0, 0, 0], [w, h, 0], [0, h, 0]],
+                            dtype=np.float32)
+            quads = np.empty((grid[1], grid[0], 6, 3), dtype=np.float32)
+            quads[:] = quad
+
+            mgrid = np.mgrid[0.:grid[1], 0.:grid[0]].transpose(1, 2, 0)
+            mgrid = mgrid[:, :, np.newaxis, :]
+            mgrid[..., 0] *= w
+            mgrid[..., 1] *= h
+
+            quads[..., :2] += mgrid
+            tex_coords = quads.reshape(grid[1]*grid[0]*6, 3)
+            tex_coords = np.ascontiguousarray(tex_coords[:, :2])
+            vertices = tex_coords * self.size
+            
+            # vertex shader provides correct texture coordinates
+            self._program.frag['map_uv_to_tex'] = NullTransform()
+        
+        elif method == 'impostor':
+            # quad covers entire view; frag. shader will deal with image shape
+            vertices = np.array([[-1, -1], [1, -1], [1, 1],
+                                 [-1, -1], [1, 1], [-1, 1]],
+                                dtype=np.float32)
+            tex_coords = vertices
+
+            # vertex shader provides ND coordinates; 
+            # fragment shader maps to texture coordinates
+            self._program.vert['transform'] = NullTransform()
+            self._raycast_func = Function('''
+                vec4 map_local_to_tex(vec4 x) {
+                    // Cast ray from 3D viewport to surface of image
+                    // (if $transform does not affect z values, then this
+                    // can be optimized as simply $transform.map(x) )
+                    vec4 p1 = $transform(x);
+                    vec4 p2 = $transform(x + vec4(0, 0, 0.5, 0));
+                    p1 /= p1.w;
+                    p2 /= p2.w;
+                    vec4 d = p2 - p1;
+                    float f = p2.z / d.z;
+                    vec4 p3 = p2 - d * f;
+                    
+                    // finally map local to texture coords
+                    return vec4(p3.xy / $image_size, 0, 1);
+                }
+            ''')
+            self._raycast_func['image_size'] = self.size
+            self._program.frag['map_uv_to_tex'] = self._raycast_func
+        
+        else:
+            raise ValueError("Unknown image draw method '%s'" % method)
+        
+        self._program['a_position'] = vertices.astype(np.float32)
+        self._program['a_texcoord'] = tex_coords.astype(np.float32)
+        self._need_vertex_update = False
+
+    def _build_texture(self):
+        data = self._data
+        if data.dtype == np.float64:
+            data = data.astype(np.float32)
+
+        if data.ndim == 2 or data.shape[2] == 1:
+            # deal with clim on CPU b/c of texture depth limits :(
+            # can eventually do this by simulating 32-bit float... maybe
+            clim = self._clim
+            if isinstance(clim, string_types) and clim == 'auto':
+                clim = np.min(data), np.max(data)
+            clim = np.asarray(clim, dtype=np.float32)
+            data = data - clim[0]  # not inplace so we don't modify orig data
+            if clim[1] - clim[0] > 0:
+                data /= clim[1] - clim[0]
+            else:
+                data[:] = 1 if data[0, 0] != 0 else 0
+            fun = FunctionChain(None, [Function(_c2l),
+                                       Function(self.cmap.glsl_map)])
+            self._clim = np.array(clim)
+        else:
+            fun = Function(_null_color_transform)
+        self._program.frag['color_transform'] = fun
+        self._texture = Texture2D(data, interpolation=self._interpolation)
+        self._program['u_texture'] = self._texture
+
+    def bounds(self, mode, axis):
+        """Get the bounds
+
+        Parameters
+        ----------
+        mode : str
+            Describes the type of boundary requested. Can be "visual", "data",
+            or "mouse".
+        axis : 0, 1, 2
+            The axis along which to measure the bounding values, in
+            x-y-z order.
+        """
+        if axis > 1:
+            return (0, 0)
+        else:
+            return (0, self.size[axis])
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        if self._data is None:
+            return
+
+        set_state(cull_face=False)
+
+        # upload texture is needed
+        if self._texture is None:
+            self._build_texture()
+
+        # rebuild vertex buffers if needed
+        if self._need_vertex_update:
+            self._build_vertex_data(transforms)
+
+        # update transform
+        method = self._method_used
+        if method == 'subdivide':
+            self._program.vert['transform'] = transforms.get_full_transform()
+        else:
+            self._raycast_func['transform'] = \
+                transforms.get_full_transform().inverse
+
+        self._program.draw('triangles')
diff --git a/vispy/scene/visuals/isocurve.py b/vispy/visuals/isocurve.py
similarity index 72%
rename from vispy/scene/visuals/isocurve.py
rename to vispy/visuals/isocurve.py
index 5791aa5..e7f870d 100644
--- a/vispy/scene/visuals/isocurve.py
+++ b/vispy/visuals/isocurve.py
@@ -1,16 +1,16 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
 
 import numpy as np
 
-from .line import Line
-from ...geometry.isocurve import isocurve
+from .line import LineVisual
+from ..geometry.isocurve import isocurve
 
 
-class Isocurve(Line):
+class IsocurveVisual(LineVisual):
     """Displays an isocurve of a 2D scalar array.
 
     Parameters
@@ -23,23 +23,23 @@ class Isocurve(Line):
     Notes
     -----
     """
-    def __init__(self, data=None, level=None, **kwds):
+    def __init__(self, data=None, level=None, **kwargs):
         self._data = None
         self._level = level
         self._recompute = True
-        kwds['mode'] = 'gl'
-        kwds['antialias'] = False
-        Line.__init__(self, **kwds)
+        kwargs['method'] = 'gl'
+        kwargs['antialias'] = False
+        LineVisual.__init__(self, **kwargs)
         if data is not None:
             self.set_data(data)
 
     @property
     def level(self):
-        """ The threshold at which the isocurve is constructed from the 
+        """ The threshold at which the isocurve is constructed from the
         2D data.
         """
         return self._level
-    
+
     @level.setter
     def level(self, level):
         self._level = level
@@ -49,8 +49,8 @@ class Isocurve(Line):
     def set_data(self, data):
         """ Set the scalar array data
 
-        Parameters:
-        -----------
+        Parameters
+        ----------
         data : ndarray
             A 2D array of scalar values. The isocurve is constructed to show
             all locations in the scalar field equal to ``self.level``.
@@ -59,13 +59,20 @@ class Isocurve(Line):
         self._recompute = True
         self.update()
 
-    def draw(self, event):
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
         if self._data is None or self._level is None:
             return
-        
+
         if self._recompute:
             verts = []
-            paths = isocurve(self._data.astype(float).T, self._level, 
+            paths = isocurve(self._data.astype(float).T, self._level,
                              extend_to_edge=True, connected=True)
             tot = 0
             gaps = []
@@ -73,12 +80,12 @@ class Isocurve(Line):
                 verts.extend(path)
                 tot += len(path)
                 gaps.append(tot-1)
-                
+
             connect = np.ones(tot-1, dtype=bool)
             connect[gaps[:-1]] = False
-            
+
             verts = np.array(verts)
-            Line.set_data(self, pos=verts, connect=connect)
+            LineVisual.set_data(self, pos=verts, connect=connect)
             self._recompute = False
-            
-        Line.draw(self, event)
+
+        LineVisual.draw(self, transforms)
diff --git a/vispy/visuals/isoline.py b/vispy/visuals/isoline.py
new file mode 100644
index 0000000..d53d7c7
--- /dev/null
+++ b/vispy/visuals/isoline.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division
+
+import numpy as np
+
+from .line import LineVisual
+from ..color import ColorArray
+from ..ext.six import string_types
+from ..color.colormap import _normalize, get_colormap
+
+
+def iso_mesh_line(vertices, tris, vertex_data, levels):
+    """Generate an isocurve from vertex data in a surface mesh.
+
+    Parameters
+    ----------
+    vertices : ndarray, shape (Nv, 3)
+        Vertex coordinates.
+    tris : ndarray, shape (Nf, 3)
+        Indices of triangular element into the vertices array.
+    vertex_data : ndarray, shape (Nv,)
+        data at vertex.
+    levels : ndarray, shape (Nl,)
+        Levels at which to generate an isocurve
+
+    Returns
+    -------
+    lines : ndarray, shape (Nvout, 3)
+        Vertex coordinates for lines points
+    connects : ndarray, shape (Ne, 2)
+        Indices of line element into the vertex array.
+    vertex_level: ndarray, shape (Nvout,)
+        level for vertex in lines
+
+    Notes
+    -----
+    Uses a marching squares algorithm to generate the isolines.
+    """
+
+    lines = None
+    connects = None
+    vertex_level = None
+    if not all([isinstance(x, np.ndarray) for x in (vertices, tris,
+                vertex_data, levels)]):
+        raise ValueError('all inputs must be numpy arrays')
+    if vertices.shape[1] <= 3:
+        verts = vertices
+    elif vertices.shape[1] == 4:
+        verts = vertices[:, :-1]
+    else:
+        verts = None
+    if (verts is not None and tris.shape[1] == 3 and
+            vertex_data.shape[0] == verts.shape[0]):
+        edges = np.vstack((tris.reshape((-1)),
+                           np.roll(tris, -1, axis=1).reshape((-1)))).T
+        edge_datas = vertex_data[edges]
+        edge_coors = verts[edges].reshape(tris.shape[0]*3, 2, 3)
+        for lev in levels:
+            # index for select edges with vertices have only False - True
+            # or True - False at extremity
+            index = (edge_datas >= lev)
+            index = index[:, 0] ^ index[:, 1]  # xor calculation
+            # Selectect edge
+            edge_datas_Ok = edge_datas[index, :]
+            xyz = edge_coors[index]
+            # Linear interpolation
+            ratio = np.array([(lev - edge_datas_Ok[:, 0]) /
+                              (edge_datas_Ok[:, 1] - edge_datas_Ok[:, 0])])
+            point = xyz[:, 0, :] + ratio.T*(xyz[:, 1, :] - xyz[:, 0, :])
+            nbr = point.shape[0]//2
+            if connects is not None:
+                connect = np.arange(0, nbr*2).reshape((nbr, 2)) + \
+                    len(lines)
+                connects = np.append(connects, connect, axis=0)
+                lines = np.append(lines, point, axis=0)
+                vertex_level = np.append(vertex_level,
+                                         np.zeros(len(point)) +
+                                         lev)
+            else:
+                lines = point
+                connects = np.arange(0, nbr*2).reshape((nbr, 2)) + \
+                    len(lines)
+                vertex_level = np.zeros(len(point)) + lev
+            vertex_level = vertex_level.reshape((vertex_level.size, 1))
+
+    return lines, connects, vertex_level
+
+
+class IsolineVisual(LineVisual):
+    """Isocurves of a tri mesh with data at vertices at different levels.
+
+    Parameters
+    ----------
+    vertices : ndarray, shape (Nv, 3) | None
+        Vertex coordinates.
+    tris : ndarray, shape (Nf, 3) | None
+        Indices into the vertex array.
+    data : ndarray, shape (Nv,) | None
+        scalar at vertices
+    levels : ndarray, shape (Nlev,) | None
+        The levels at which the isocurve is constructed from "data".
+    color_lev : Color, tuple, colormap name or array
+        The color to use when drawing the line. If an array is given, it
+        must be of shape (Nlev, 4) and provide one rgba color by level.
+    **kwargs : dict
+        Keyword arguments to pass to `LineVisual`.
+    """
+    def __init__(self, vertices=None, tris=None, data=None,
+                 levels=None, color_lev=None, **kwargs):
+        self._data = None
+        self._vertices = None
+        self._tris = None
+        self._levels = levels
+        self._color_lev = color_lev
+        self._update_color_lev = True
+        self._recompute = True
+        kwargs['antialias'] = False
+        LineVisual.__init__(self, method='gl', **kwargs)
+        self.set_data(vertices=vertices, tris=tris, data=data)
+
+    @property
+    def levels(self):
+        """ The threshold at which the isocurves are constructed from the data.
+        """
+        return self._levels
+
+    @levels.setter
+    def levels(self, levels):
+        self._levels = levels
+        self._recompute = True
+        self.update()
+
+    @property
+    def data(self):
+        """The mesh data"""
+        return self._vertices, self._tris, self._data
+
+    def set_data(self, vertices=None, tris=None, data=None):
+        """Set the data
+
+        Parameters
+        ----------
+        vertices : ndarray, shape (Nv, 3) | None
+            Vertex coordinates.
+        tris : ndarray, shape (Nf, 3) | None
+            Indices into the vertex array.
+        data : ndarray, shape (Nv,) | None
+            scalar at vertices
+        """
+        # modifier pour tenier compte des None self._recompute = True
+        if data is not None:
+            self._data = data
+            self._recompute = True
+        if vertices is not None:
+            self._vertices = vertices
+            self._recompute = True
+        if tris is not None:
+            self._tris = tris
+            self._recompute = True
+        self.update()
+
+    @property
+    def color(self):
+        return self._color_lev
+
+    def set_color(self, color):
+        """Set the color
+
+        Parameters
+        ----------
+        color : instance of Color
+            The color to use.
+        """
+        if color is not None:
+            self._color_lev = color
+            self._update_color_lev = True
+            self.update()
+
+    def _levels_to_colors(self):
+        if isinstance(self._color_lev, string_types):
+            f_color_levs = get_colormap(self._color_lev)
+            lev = _normalize(self._vl, self._vl.min(), self._vl.max())
+            colors = f_color_levs.map(lev)
+        else:
+            colors = ColorArray(self._color_lev).rgba
+            if len(colors) == 1:
+                colors = colors[0]
+        return colors
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        if (self._data is None or self._levels is None or self._tris is None or
+           self._vertices is None or self._color_lev is None):
+            return
+
+        if self._recompute:
+            self._v, self._c, self._vl = iso_mesh_line(self._vertices,
+                                                       self._tris, self._data,
+                                                       self._levels)
+            self._cl = self._levels_to_colors()
+            self._recompute = False
+            self._update_color_lev = False
+
+        if self._update_color_lev:
+            self._cl = self._levels_to_colors()
+            self._update_color_lev = False
+
+        LineVisual.set_data(self, pos=self._v, connect=self._c, color=self._cl)
+        LineVisual.draw(self, transforms)
diff --git a/vispy/scene/visuals/isosurface.py b/vispy/visuals/isosurface.py
similarity index 68%
rename from vispy/scene/visuals/isosurface.py
rename to vispy/visuals/isosurface.py
index d8e4b03..1abe31d 100644
--- a/vispy/scene/visuals/isosurface.py
+++ b/vispy/visuals/isosurface.py
@@ -1,14 +1,14 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
 
-from .mesh import Mesh
-from ...geometry.isosurface import isosurface
+from .mesh import MeshVisual
+from ..geometry.isosurface import isosurface
 
 
-class Isosurface(Mesh):
+class IsosurfaceVisual(MeshVisual):
     """Displays an isosurface of a 3D scalar array.
 
     Parameters
@@ -21,21 +21,21 @@ class Isosurface(Mesh):
     Notes
     -----
     """
-    def __init__(self, data=None, level=None, **kwds):
+    def __init__(self, data=None, level=None, **kwargs):
         self._data = None
         self._level = level
         self._recompute = True
-        Mesh.__init__(self, **kwds)
+        MeshVisual.__init__(self, **kwargs)
         if data is not None:
             self.set_data(data)
 
     @property
     def level(self):
-        """ The threshold at which the isosurface is constructed from the 
+        """ The threshold at which the isosurface is constructed from the
         3D data.
         """
         return self._level
-    
+
     @level.setter
     def level(self, level):
         self._level = level
@@ -45,8 +45,8 @@ class Isosurface(Mesh):
     def set_data(self, data):
         """ Set the scalar array data
 
-        Parameters:
-        -----------
+        Parameters
+        ----------
         data : ndarray
             A 3D array of scalar values. The isosurface is constructed to show
             all locations in the scalar field equal to ``self.level``.
@@ -55,13 +55,20 @@ class Isosurface(Mesh):
         self._recompute = True
         self.update()
 
-    def draw(self, event):
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
         if self._data is None or self._level is None:
             return
-        
+
         if self._recompute:
             verts, faces = isosurface(self._data, self._level)
-            Mesh.set_data(self, vertices=verts, faces=faces)
+            MeshVisual.set_data(self, vertices=verts, faces=faces)
             self._recompute = False
-            
-        Mesh.draw(self, event)
+
+        MeshVisual.draw(self, transforms)
diff --git a/vispy/scene/visuals/line/__init__.py b/vispy/visuals/line/__init__.py
similarity index 54%
rename from vispy/scene/visuals/line/__init__.py
rename to vispy/visuals/line/__init__.py
index b4cef19..112de5d 100644
--- a/vispy/scene/visuals/line/__init__.py
+++ b/vispy/visuals/line/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
-from .line import Line  # noqa
+from .line import LineVisual  # noqa
diff --git a/vispy/scene/visuals/line/dash_atlas.py b/vispy/visuals/line/dash_atlas.py
similarity index 98%
rename from vispy/scene/visuals/line/dash_atlas.py
rename to vispy/visuals/line/dash_atlas.py
index f30e1e8..005ad69 100644
--- a/vispy/scene/visuals/line/dash_atlas.py
+++ b/vispy/visuals/line/dash_atlas.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
diff --git a/vispy/scene/visuals/line/fragment.py b/vispy/visuals/line/fragment.py
similarity index 100%
rename from vispy/scene/visuals/line/fragment.py
rename to vispy/visuals/line/fragment.py
diff --git a/vispy/visuals/line/line.py b/vispy/visuals/line/line.py
new file mode 100644
index 0000000..4bee63a
--- /dev/null
+++ b/vispy/visuals/line/line.py
@@ -0,0 +1,555 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+"""
+Line visual implementing Agg- and GL-based drawing modes.
+"""
+
+from __future__ import division
+
+import numpy as np
+
+from ... import gloo
+from ...color import Color, ColorArray, get_colormap
+from ...ext.six import string_types
+from ..shaders import ModularProgram, Function
+from ..visual import Visual
+from ...util.profiler import Profiler
+
+from .dash_atlas import DashAtlas
+from . import vertex
+from . import fragment
+
+
+vec2to4 = Function("""
+    vec4 vec2to4(vec2 inp) {
+        return vec4(inp, 0, 1);
+    }
+""")
+
+vec3to4 = Function("""
+    vec4 vec3to4(vec3 inp) {
+        return vec4(inp, 1);
+    }
+""")
+
+
+"""
+TODO:
+
+* Agg support is very minimal; needs attention.
+* Optimization--avoid creating new buffers, avoid triggering program
+  recompile.
+"""
+
+
+joins = {'miter': 0, 'round': 1, 'bevel': 2}
+
+caps = {'': 0, 'none': 0, '.': 0,
+        'round': 1, ')': 1, '(': 1, 'o': 1,
+        'triangle in': 2, '<': 2,
+        'triangle out': 3, '>': 3,
+        'square': 4, '=': 4, 'butt': 4,
+        '|': 5}
+
+
+class LineVisual(Visual):
+    """Line visual
+
+    Parameters
+    ----------
+    pos : array
+        Array of shape (..., 2) or (..., 3) specifying vertex coordinates.
+    color : Color, tuple, or array
+        The color to use when drawing the line. If an array is given, it
+        must be of shape (..., 4) and provide one rgba color per vertex.
+        Can also be a colormap name, or appropriate `Function`.
+    width:
+        The width of the line in px. Line widths > 1px are only
+        guaranteed to work when using 'agg' method.
+    connect : str or array
+        Determines which vertices are connected by lines.
+
+            * "strip" causes the line to be drawn with each vertex
+              connected to the next.
+            * "segments" causes each pair of vertices to draw an
+              independent line segment
+            * numpy arrays specify the exact set of segment pairs to
+              connect.
+
+    method : str
+        Mode to use for drawing.
+
+            * "agg" uses anti-grain geometry to draw nicely antialiased lines
+              with proper joins and endcaps.
+            * "gl" uses OpenGL's built-in line rendering. This is much faster,
+              but produces much lower-quality results and is not guaranteed to
+              obey the requested line width or join/endcap styles.
+
+    antialias : bool
+        Enables or disables antialiasing.
+        For method='gl', this specifies whether to use GL's line smoothing, 
+        which may be unavailable or inconsistent on some platforms.
+    """
+    def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1,
+                 connect='strip', method='gl', antialias=False):
+        Visual.__init__(self)
+
+        self._changed = {'pos': False, 'color': False, 'width': False,
+                         'connect': False}
+        
+        self._pos = None
+        self._color = None
+        self._width = None
+        self._connect = None
+        self._bounds = None
+        
+        # don't call subclass set_data; these often have different
+        # signatures.
+        LineVisual.set_data(self, pos=pos, color=color, width=width,
+                            connect=connect)
+        self._method = 'none'
+        self.antialias = antialias
+        self.method = method
+
+    @property
+    def _program(self):
+        return self._line_visual._program
+
+    @property
+    def antialias(self):
+        return self._antialias
+
+    @antialias.setter
+    def antialias(self, aa):
+        self._antialias = bool(aa)
+        self.update()
+
+    @property
+    def method(self):
+        """The current drawing method"""
+        return self._method
+
+    @method.setter
+    def method(self, method):
+        if method not in ('agg', 'gl'):
+            raise ValueError('method argument must be "agg" or "gl".')
+        if method == self._method:
+            return
+
+        self._method = method
+        if method == 'gl':
+            self._line_visual = _GLLineVisual(self)
+        elif method == 'agg':
+            self._line_visual = _AggLineVisual(self)
+
+        for k in self._changed:
+            self._changed[k] = True
+
+    def set_data(self, pos=None, color=None, width=None, connect=None):
+        """ Set the data used to draw this visual.
+
+        Parameters
+        ----------
+        pos : array
+            Array of shape (..., 2) or (..., 3) specifying vertex coordinates.
+        color : Color, tuple, or array
+            The color to use when drawing the line. If an array is given, it
+            must be of shape (..., 4) and provide one rgba color per vertex.
+        width:
+            The width of the line in px. Line widths > 1px are only
+            guaranteed to work when using 'agg' method.
+        connect : str or array
+            Determines which vertices are connected by lines.
+            * "strip" causes the line to be drawn with each vertex
+              connected to the next.
+            * "segments" causes each pair of vertices to draw an
+              independent line segment
+            * int numpy arrays specify the exact set of segment pairs to
+              connect.
+            * bool numpy arrays specify which _adjacent_ pairs to connect.
+        """
+        if pos is not None:
+            self._bounds = None
+            self._pos = pos
+            self._changed['pos'] = True
+
+        if color is not None:
+            self._color = color
+            self._changed['color'] = True
+
+        if width is not None:
+            self._width = width
+            self._changed['width'] = True
+
+        if connect is not None:
+            self._connect = connect
+            self._changed['connect'] = True
+
+        self.update()
+
+    @property
+    def color(self):
+        return self._color
+
+    @property
+    def width(self):
+        return self._width
+
+    @property
+    def connect(self):
+        return self._connect
+
+    @property
+    def pos(self):
+        return self._pos
+
+    def _interpret_connect(self):
+        if isinstance(self._connect, np.ndarray):
+            # Convert a boolean connection array to a vertex index array
+            if self._connect.ndim == 1 and self._connect.dtype == bool:
+                index = np.empty((len(self._connect), 2), dtype=np.uint32)
+                index[:] = np.arange(len(self._connect))[:, np.newaxis]
+                index[:, 1] += 1
+                return index[self._connect]
+            elif self._connect.ndim == 2 and self._connect.shape[1] == 2:
+                return self._connect.astype(np.uint32)
+            else:
+                raise TypeError("Got invalid connect array of shape %r and "
+                                "dtype %r" % (self._connect.shape,
+                                              self._connect.dtype))
+        else:
+            return self._connect
+
+    def _interpret_color(self):
+        if isinstance(self._color, string_types):
+            try:
+                colormap = get_colormap(self._color)
+                color = Function(colormap.glsl_map)
+            except KeyError:
+                color = Color(self._color).rgba
+        elif isinstance(self._color, Function):
+            color = Function(self._color)
+        else:
+            color = ColorArray(self._color).rgba
+            if len(color) == 1:
+                color = color[0]
+        return color
+
+    def bounds(self, mode, axis):
+        """Get the bounds
+
+        Parameters
+        ----------
+        mode : str
+            Describes the type of boundary requested. Can be "visual", "data",
+            or "mouse".
+        axis : 0, 1, 2
+            The axis along which to measure the bounding values, in
+            x-y-z order.
+        """
+        # Can and should we calculate bounds?
+        if (self._bounds is None) and self._pos is not None:
+            pos = self._pos
+            self._bounds = [(pos[:, d].min(), pos[:, d].max())
+                            for d in range(pos.shape[1])]
+        # Return what we can
+        if self._bounds is None:
+            return
+        else:
+            if axis < len(self._bounds):
+                return self._bounds[axis]
+            else:
+                return (0, 0)
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        if self.width == 0:
+            return
+        self._line_visual.draw(transforms)
+        for k in self._changed:
+            self._changed[k] = False
+
+    def set_gl_state(self, **kwargs):
+        Visual.set_gl_state(self, **kwargs)
+        self._line_visual.set_gl_state(**kwargs)
+
+    def update_gl_state(self, **kwargs):
+        Visual.update_gl_state(self, **kwargs)
+        self._line_visual.update_gl_state(**kwargs)
+
+
+class _GLLineVisual(Visual):
+    VERTEX_SHADER = """
+        varying vec4 v_color;
+
+        void main(void) {
+            gl_Position = $transform($to_vec4($position));
+            v_color = $color;
+        }
+    """
+
+    FRAGMENT_SHADER = """
+        varying vec4 v_color;
+        void main() {
+            gl_FragColor = v_color;
+        }
+    """
+
+    def __init__(self, parent):
+        self._parent = parent
+        self._pos_vbo = gloo.VertexBuffer()
+        self._color_vbo = gloo.VertexBuffer()
+        self._connect_ibo = gloo.IndexBuffer()
+        self._connect = None
+        
+        # Set up the GL program
+        self._program = ModularProgram(self.VERTEX_SHADER,
+                                       self.FRAGMENT_SHADER)
+        self.set_gl_state('translucent')
+
+    def draw(self, transforms):
+        prof = Profiler()
+        Visual.draw(self, transforms)
+        
+        # first see whether we can bail out early
+        if self._parent._width <= 0:
+            return
+        
+        if self._parent._changed['pos']:
+            if self._parent._pos is None:
+                return
+            # todo: does this result in unnecessary copies?
+            pos = np.ascontiguousarray(self._parent._pos.astype(np.float32))
+            self._pos_vbo.set_data(pos)
+            self._program.vert['position'] = self._pos_vbo
+            if pos.shape[-1] == 2:
+                self._program.vert['to_vec4'] = vec2to4
+            elif pos.shape[-1] == 3:
+                self._program.vert['to_vec4'] = vec3to4
+            else:
+                raise TypeError("Got bad position array shape: %r"
+                                % (pos.shape,))
+
+        if self._parent._changed['color']:
+            color = self._parent._interpret_color()
+            # If color is not visible, just quit now
+            if isinstance(color, Color) and color.is_blank:
+                return
+            if isinstance(color, Function):
+                # TODO: Change to the parametric coordinate once that is done
+                self._program.vert['color'] = color(
+                    '(gl_Position.x + 1.0) / 2.0')
+            else:
+                if color.ndim == 1:
+                    self._program.vert['color'] = color
+                else:
+                    self._color_vbo.set_data(color)
+                    self._program.vert['color'] = self._color_vbo
+
+        xform = transforms.get_full_transform()
+        self._program.vert['transform'] = xform
+
+        # Do we want to use OpenGL, and can we?
+        GL = None
+        try:
+            import OpenGL.GL as GL
+        except ImportError:
+            pass
+
+        # Turn on line smooth and/or line width
+        if GL:
+            if self._parent._antialias:
+                GL.glEnable(GL.GL_LINE_SMOOTH)
+            else:
+                GL.glDisable(GL.GL_LINE_SMOOTH)
+            # this is a bit of a hack to deal with HiDPI
+            tr = transforms.document_to_framebuffer
+            px_scale = np.mean((tr.map((1, 0)) - tr.map((0, 1)))[:2])
+            width = px_scale * self._parent._width
+            GL.glLineWidth(max(width, 1.))
+
+        if self._parent._changed['connect']:
+            self._connect = self._parent._interpret_connect()
+            if isinstance(self._connect, np.ndarray):
+                self._connect_ibo.set_data(self._connect)
+        if self._connect is None:
+            return
+        
+        prof('prepare')
+
+        # Draw
+        if self._connect == 'strip':
+            self._program.draw('line_strip')
+        elif self._connect == 'segments':
+            self._program.draw('lines')
+        elif isinstance(self._connect, np.ndarray):
+            self._program.draw('lines', self._connect_ibo)
+        else:
+            raise ValueError("Invalid line connect mode: %r" % self._connect)
+        
+        prof('draw')
+
+
+class _AggLineVisual(Visual):
+    _agg_vtype = np.dtype([('a_position', 'f4', 2),
+                           ('a_tangents', 'f4', 4),
+                           ('a_segment',  'f4', 2),
+                           ('a_angles',   'f4', 2),
+                           ('a_texcoord', 'f4', 2),
+                           ('alength', 'f4', 1),
+                           ('color', 'f4', 4)])
+
+    def __init__(self, parent):
+        self._parent = parent
+        self._vbo = gloo.VertexBuffer()
+        self._ibo = gloo.IndexBuffer()
+
+        self._pos = None
+        self._color = None
+        self._program = ModularProgram(vertex.VERTEX_SHADER,
+                                       fragment.FRAGMENT_SHADER)
+
+        self._da = DashAtlas()
+        dash_index, dash_period = self._da['solid']
+        self._U = dict(dash_index=dash_index, dash_period=dash_period,
+                       linejoin=joins['round'],
+                       linecaps=(caps['round'], caps['round']),
+                       dash_caps=(caps['round'], caps['round']),
+                       antialias=1.0)
+        self._dash_atlas = gloo.Texture2D(self._da._data)
+        self.set_gl_state('translucent')
+
+    def draw(self, transforms):
+        Visual.draw(self, transforms)
+        
+        bake = False
+        if self._parent._changed['pos']:
+            if self._parent._pos is None:
+                return
+            # todo: does this result in unnecessary copies?
+            self._pos = np.ascontiguousarray(
+                self._parent._pos.astype(np.float32))
+            bake = True
+
+        if self._parent._changed['color']:
+            self._color = self._parent._interpret_color()
+            bake = True
+
+        if self._parent._changed['connect']:
+            if self._parent._connect not in [None, 'strip']:
+                raise NotImplementedError("Only 'strip' connection mode "
+                                          "allowed for agg-method lines.")
+
+        if bake:
+            V, I = self._agg_bake(self._pos, self._color)
+            self._vbo.set_data(V)
+            self._ibo.set_data(I)
+
+        gloo.set_state('translucent', depth_test=False)
+        data_doc = transforms.visual_to_document
+        doc_px = transforms.document_to_framebuffer
+        px_ndc = transforms.framebuffer_to_render
+
+        vert = self._program.vert
+        vert['doc_px_transform'] = doc_px
+        vert['px_ndc_transform'] = px_ndc
+        vert['transform'] = data_doc
+
+        #self._program.prepare()
+        self._program.bind(self._vbo)
+        uniforms = dict(closed=False, miter_limit=4.0, dash_phase=0.0,
+                        linewidth=self._parent._width)
+        for n, v in uniforms.items():
+            self._program[n] = v
+        for n, v in self._U.items():
+            self._program[n] = v
+        self._program['u_dash_atlas'] = self._dash_atlas
+        self._program.draw('triangles', self._ibo)
+
+    @classmethod
+    def _agg_bake(cls, vertices, color, closed=False):
+        """
+        Bake a list of 2D vertices for rendering them as thick line. Each line
+        segment must have its own vertices because of antialias (this means no
+        vertex sharing between two adjacent line segments).
+        """
+
+        n = len(vertices)
+        P = np.array(vertices).reshape(n, 2).astype(float)
+        idx = np.arange(n)  # used to eventually tile the color array
+
+        dx, dy = P[0] - P[-1]
+        d = np.sqrt(dx*dx+dy*dy)
+
+        # If closed, make sure first vertex = last vertex (+/- epsilon=1e-10)
+        if closed and d > 1e-10:
+            P = np.append(P, P[0]).reshape(n+1, 2)
+            idx = np.append(idx, idx[-1])
+            n += 1
+
+        V = np.zeros(len(P), dtype=cls._agg_vtype)
+        V['a_position'] = P
+
+        # Tangents & norms
+        T = P[1:] - P[:-1]
+
+        N = np.sqrt(T[:, 0]**2 + T[:, 1]**2)
+        # T /= N.reshape(len(T),1)
+        V['a_tangents'][+1:, :2] = T
+        V['a_tangents'][0, :2] = T[-1] if closed else T[0]
+        V['a_tangents'][:-1, 2:] = T
+        V['a_tangents'][-1, 2:] = T[0] if closed else T[-1]
+
+        # Angles
+        T1 = V['a_tangents'][:, :2]
+        T2 = V['a_tangents'][:, 2:]
+        A = np.arctan2(T1[:, 0]*T2[:, 1]-T1[:, 1]*T2[:, 0],
+                       T1[:, 0]*T2[:, 0]+T1[:, 1]*T2[:, 1])
+        V['a_angles'][:-1, 0] = A[:-1]
+        V['a_angles'][:-1, 1] = A[+1:]
+
+        # Segment
+        L = np.cumsum(N)
+        V['a_segment'][+1:, 0] = L
+        V['a_segment'][:-1, 1] = L
+        #V['a_lengths'][:,2] = L[-1]
+
+        # Step 1: A -- B -- C  =>  A -- B, B' -- C
+        V = np.repeat(V, 2, axis=0)[1:-1]
+        V['a_segment'][1:] = V['a_segment'][:-1]
+        V['a_angles'][1:] = V['a_angles'][:-1]
+        V['a_texcoord'][0::2] = -1
+        V['a_texcoord'][1::2] = +1
+        idx = np.repeat(idx, 2)[1:-1]
+
+        # Step 2: A -- B, B' -- C  -> A0/A1 -- B0/B1, B'0/B'1 -- C0/C1
+        V = np.repeat(V, 2, axis=0)
+        V['a_texcoord'][0::2, 1] = -1
+        V['a_texcoord'][1::2, 1] = +1
+        idx = np.repeat(idx, 2)
+
+        I = np.resize(np.array([0, 1, 2, 1, 2, 3], dtype=np.uint32),
+                      (n-1)*(2*3))
+        I += np.repeat(4*np.arange(n-1, dtype=np.uint32), 6)
+
+        # Length
+        V['alength'] = L[-1] * np.ones(len(V))
+
+        # Color
+        if color.ndim == 1:
+            color = np.tile(color, (len(V), 1))
+        elif color.ndim == 2 and len(color) == n:
+            color = color[idx]
+        else:
+            raise ValueError('Color length %s does not match number of '
+                             'vertices %s' % (len(color), n))
+        V['color'] = color
+
+        return V, I
diff --git a/vispy/scene/visuals/line/vertex.py b/vispy/visuals/line/vertex.py
similarity index 100%
rename from vispy/scene/visuals/line/vertex.py
rename to vispy/visuals/line/vertex.py
diff --git a/vispy/visuals/line_plot.py b/vispy/visuals/line_plot.py
new file mode 100644
index 0000000..f1d2ca8
--- /dev/null
+++ b/vispy/visuals/line_plot.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+import numpy as np
+
+from .line import LineVisual
+from .markers import MarkersVisual
+from .visual import Visual
+
+
+class LinePlotVisual(Visual):
+    """Visual displaying a plot line with optional markers.
+
+    Parameters
+    ----------
+    data : array-like
+        Arguments can be passed as ``(Y,)``, ``(X, Y)`` or
+        ``np.array((X, Y))``.
+    color : instance of Color
+        Color of the line.
+    symbol : str
+        Marker symbol to use.
+    line_kind : str
+        Kind of line to draw. For now, only solid lines (``'-'``)
+        are supported.
+    width : float
+        Line width.
+    marker_size : float
+        Marker size. If `size == 0` markers will not be shown.
+    edge_color : instance of Color
+        Color of the marker edge.
+    face_color : instance of Color
+        Color of the marker face.
+    edge_width : float
+        Edge width of the marker.
+    connect : str | array
+        See LineVisual.
+    **kwargs : keyword arguments
+        Argements to pass to the super class.
+
+    Examples
+    --------
+    All of these syntaxes will work:
+
+        >>> LinePlotVisual(y_vals)
+        >>> LinePlotVisual(x_vals, y_vals)
+        >>> LinePlotVisual(xy_vals)
+
+    See also
+    --------
+    LineVisual, MarkersVisual, marker_types
+    """
+    _line_kwargs = ('color', 'width', 'connect')
+    _marker_kwargs = ('edge_color', 'face_color', 'edge_width',
+                      'marker_size', 'symbol')
+    _kw_trans = dict(marker_size='size')
+
+    def __init__(self, data, color='k', symbol='o', line_kind='-',
+                 width=1., marker_size=10., edge_color='k', face_color='w',
+                 edge_width=1., connect='strip', **kwargs):
+        Visual.__init__(self, **kwargs)
+        if line_kind != '-':
+            raise ValueError('Only solid lines currently supported')
+        self._line = LineVisual()
+        self._markers = MarkersVisual()
+        self.set_data(data, color=color, symbol=symbol,
+                      width=width, marker_size=marker_size,
+                      edge_color=edge_color, face_color=face_color,
+                      edge_width=edge_width, connect=connect)
+
+    def set_data(self, data, **kwargs):
+        """Set the line data
+
+        Parameters
+        ----------
+        data : array-like
+            The data.
+        **kwargs : dict
+            Keywoard arguments to pass to MarkerVisual and LineVisal.
+        """
+        pos = np.atleast_1d(data).astype(np.float32)
+        if pos.ndim == 1:
+            pos = pos[:, np.newaxis]
+        elif pos.ndim > 2:
+            raise ValueError('data must have at most two dimensions')
+
+        if pos.size == 0:
+            pos = self._line.pos
+
+            # if both args and keywords are zero, then there is no
+            # point in calling this function.
+            if len(kwargs) == 0:
+                raise TypeError("neither line points nor line properties"
+                                "are provided")
+        elif pos.shape[1] == 1:
+            x = np.arange(pos.shape[0], dtype=np.float32)[:, np.newaxis]
+            pos = np.concatenate((x, pos), axis=1)
+        # if args are empty, don't modify position
+        elif pos.shape[1] > 2:
+            raise TypeError("Too many coordinates given (%s; max is 2)."
+                            % pos.shape[1])
+
+        # todo: have both sub-visuals share the same buffers.
+        line_kwargs = {}
+        for k in self._line_kwargs:
+            if k in kwargs:
+                k_ = self._kw_trans[k] if k in self._kw_trans else k
+                line_kwargs[k] = kwargs.pop(k_)
+        self._line.set_data(pos=pos, **line_kwargs)
+        marker_kwargs = {}
+        for k in self._marker_kwargs:
+            if k in kwargs:
+                k_ = self._kw_trans[k] if k in self._kw_trans else k
+                marker_kwargs[k_] = kwargs.pop(k)
+        self._markers.set_data(pos=pos, **marker_kwargs)
+        if len(kwargs) > 0:
+            raise TypeError("Invalid keyword arguments: %s" % kwargs.keys())
+
+    def bounds(self, mode, axis):
+        """Get the bounds
+
+        Parameters
+        ----------
+        mode : str
+            Describes the type of boundary requested. Can be "visual", "data",
+            or "mouse".
+        axis : 0, 1, 2
+            The axis along which to measure the bounding values, in
+            x-y-z order.
+        """
+        return self._line.bounds(mode, axis)
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        for v in self._line, self._markers:
+            v.draw(transforms)
diff --git a/vispy/visuals/markers.py b/vispy/visuals/markers.py
new file mode 100644
index 0000000..45c1025
--- /dev/null
+++ b/vispy/visuals/markers.py
@@ -0,0 +1,648 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Marker Visual and shader definitions.
+"""
+
+import numpy as np
+
+from ..color import ColorArray
+from ..gloo import VertexBuffer, _check_valid
+from .shaders import ModularProgram, Function, Variable
+from .visual import Visual
+
+
+vert = """
+uniform mat4 u_projection;
+uniform float u_antialias;
+uniform int u_px_scale;
+
+attribute vec3  a_position;
+attribute vec4  a_fg_color;
+attribute vec4  a_bg_color;
+attribute float a_edgewidth;
+attribute float a_size;
+
+varying vec4 v_fg_color;
+varying vec4 v_bg_color;
+varying float v_edgewidth;
+varying float v_antialias;
+
+void main (void) {
+    $v_size = a_size * u_px_scale;
+    v_edgewidth = a_edgewidth;
+    v_antialias = u_antialias;
+    v_fg_color  = a_fg_color;
+    v_bg_color  = a_bg_color;
+    gl_Position = $transform(vec4(a_position,1.0));
+    float edgewidth = max(v_edgewidth, 1.0);
+    gl_PointSize = $scalarsize($v_size) + 4*(edgewidth + 1.5*v_antialias);
+}
+"""
+
+
+frag = """
+varying vec4 v_fg_color;
+varying vec4 v_bg_color;
+varying float v_edgewidth;
+varying float v_antialias;
+
+void main()
+{
+    float edgewidth = max(v_edgewidth, 1.0);
+    float edgealphafactor = min(v_edgewidth, 1.0);
+
+    float size = $scalarsize($v_size) + 4*(edgewidth + 1.5*v_antialias);
+    // factor 6 for acute edge angles that need room as for star marker
+
+    // The marker function needs to be linked with this shader
+    float r = $marker(gl_PointCoord, size);
+
+    // it takes into account an antialising layer
+    // of size v_antialias inside the edge
+    // r:
+    // [-e/2-a, -e/2+a] antialising face-edge
+    // [-e/2+a, e/2-a] core edge (center 0, diameter e-2a = 2t)
+    // [e/2-a, e/2+a] antialising edge-background
+    float t = 0.5*v_edgewidth - v_antialias;
+    float d = abs(r) - t;
+
+    vec4 edgecolor = vec4(v_fg_color.rgb, edgealphafactor*v_fg_color.a);
+
+    if (r > 0.5*v_edgewidth + v_antialias)
+    {
+        // out of the marker (beyond the outer edge of the edge
+        // including transition zone due to antialiasing)
+        discard;
+    }
+    else if (d < 0.0)
+    {
+        // inside the width of the edge
+        // (core, out of the transition zone for antialiasing)
+        gl_FragColor = edgecolor;
+    }
+    else
+    {
+        if (v_edgewidth == 0.)
+        {// no edge
+            if (r > -v_antialias)
+            {
+                float alpha = 1.0 + r/v_antialias;
+                alpha = exp(-alpha*alpha);
+                gl_FragColor = vec4(v_bg_color.rgb, alpha*v_bg_color.a);
+            }
+            else
+            {
+                gl_FragColor = v_bg_color;
+            }
+        }
+        else
+        {
+            float alpha = d/v_antialias;
+            alpha = exp(-alpha*alpha);
+            if (r > 0)
+            {
+                // outer part of the edge: fade out into the background...
+                gl_FragColor = vec4(edgecolor.rgb, alpha*edgecolor.a);
+            }
+            else
+            {
+                gl_FragColor = mix(v_bg_color, edgecolor, alpha);
+            }
+        }
+    }
+}
+"""
+
+size1d = """
+float size1d(float size)
+{
+    return size;
+}
+"""
+
+size2d = """
+float size2d(vec2 size)
+{
+    return max(size.x, size.y);
+}
+"""
+
+disc = """
+float disc(vec2 pointcoord, float size)
+{
+    float r = length((pointcoord.xy - vec2(0.5,0.5))*size);
+    r -= $v_size/2;
+    return r;
+}
+"""
+
+
+arrow = """
+const float sqrt2 = sqrt(2.);
+float rect(vec2 pointcoord, float size)
+{
+    float half_size = $v_size/2.;
+    float ady = abs(pointcoord.y -.5)*size;
+    float dx = (pointcoord.x -.5)*size;
+    float r1 = abs(dx) + ady - half_size;
+    float r2 = dx + 0.25*$v_size + ady - half_size;
+    float r = max(r1,-r2);
+    return r/sqrt2;//account for slanted edge and correct for width
+}
+"""
+
+
+ring = """
+float ring(vec2 pointcoord, float size)
+{
+    float r1 = length((pointcoord.xy - vec2(0.5,0.5))*size) - $v_size/2;
+    float r2 = length((pointcoord.xy - vec2(0.5,0.5))*size) - $v_size/4;
+    float r = max(r1,-r2);
+    return r;
+}
+"""
+
+clobber = """
+const float sqrt3 = sqrt(3.);
+float clobber(vec2 pointcoord, float size)
+{
+    const float PI = 3.14159265358979323846264;
+    const float t1 = -PI/2;
+    float circle_radius = 0.32 * $v_size;
+    float center_shift = 0.36/sqrt3 * $v_size;
+    //total size (horizontal) = 2*circle_radius + sqrt3*center_shirt = $v_size
+    vec2  c1 = vec2(cos(t1),sin(t1))*center_shift;
+    const float t2 = t1+2*PI/3;
+    vec2  c2 = vec2(cos(t2),sin(t2))*center_shift;
+    const float t3 = t2+2*PI/3;
+    vec2  c3 = vec2(cos(t3),sin(t3))*center_shift;
+    //xy is shift to center marker vertically
+    vec2 xy = (pointcoord.xy-vec2(0.5,0.5))*size + vec2(0.,-0.25*center_shift);
+    float r1 = length(xy - c1) - circle_radius;
+    float r2 = length(xy - c2) - circle_radius;
+    float r3 = length(xy - c3) - circle_radius;
+    float r = min(min(r1,r2),r3);
+    return r;
+}
+"""
+
+
+square = """
+float square(vec2 pointcoord, float size)
+{
+    float r = max(abs(pointcoord.x -.5)*size, abs(pointcoord.y -.5)*size);
+    r -= $v_size/2;
+    return r;
+}
+"""
+
+x_ = """
+float x_(vec2 pointcoord, float size)
+{
+    vec2 rotcoord = vec2((pointcoord.x + pointcoord.y - 1.) / sqrt(2.),
+                         (pointcoord.y - pointcoord.x) / sqrt(2.));
+    //vbar
+    float r1 = abs(rotcoord.x)*size - $v_size/6;
+    float r2 = abs(rotcoord.y)*size - $v_size/2;
+    float vbar = max(r1,r2);
+    //hbar
+    float r3 = abs(rotcoord.y)*size - $v_size/6;
+    float r4 = abs(rotcoord.x)*size - $v_size/2;
+    float hbar = max(r3,r4);
+    return min(vbar, hbar);
+}
+"""
+
+
+diamond = """
+float diamond(vec2 pointcoord, float size)
+{
+    float r = abs(pointcoord.x -.5)*size + abs(pointcoord.y -.5)*size;
+    r -= $v_size/2;
+    return r / sqrt(2.);//account for slanted edge and correct for width
+}
+"""
+
+
+vbar = """
+float vbar(vec2 pointcoord, float size)
+{
+    float r1 = abs(pointcoord.x - 0.5)*size - $v_size/6;
+    float r3 = abs(pointcoord.y - 0.5)*size - $v_size/2;
+    float r = max(r1,r3);
+    return r;
+}
+"""
+
+hbar = """
+float rect(vec2 pointcoord, float size)
+{
+    float r2 = abs(pointcoord.y - 0.5)*size - $v_size/6;
+    float r3 = abs(pointcoord.x - 0.5)*size - $v_size/2;
+    float r = max(r2,r3);
+    return r;
+}
+"""
+
+cross = """
+float cross(vec2 pointcoord, float size)
+{
+    //vbar
+    float r1 = abs(pointcoord.x - 0.5)*size - $v_size/6;
+    float r2 = abs(pointcoord.y - 0.5)*size - $v_size/2;
+    float vbar = max(r1,r2);
+    //hbar
+    float r3 = abs(pointcoord.y - 0.5)*size - $v_size/6;
+    float r4 = abs(pointcoord.x - 0.5)*size - $v_size/2;
+    float hbar = max(r3,r4);
+    return min(vbar, hbar);
+}
+"""
+
+
+tailed_arrow = """
+const float sqrt2 = sqrt(2.);
+float rect(vec2 pointcoord, float size)
+{
+    float half_size = $v_size/2.;
+    float ady = abs(pointcoord.y -.5)*size;
+    float dx = (pointcoord.x -.5)*size;
+    float r1 = abs(dx) + ady - half_size;
+    float r2 = dx + 0.25*$v_size + ady - half_size;
+    float arrow = max(r1,-r2);
+    //hbar
+    float upper_bottom_edges = ady - $v_size/8./sqrt2;
+    float left_edge = -dx - half_size;
+    float right_edge = dx + ady - half_size;
+    float hbar = max(upper_bottom_edges, left_edge);
+    float scale = 1.; //rescaling for slanted edge
+    if (right_edge >= hbar)
+    {
+        hbar = right_edge;
+        scale = sqrt2;
+    }
+    if (arrow <= hbar)
+    {
+        return arrow / sqrt2;//account for slanted edge and correct for width
+    }
+    else
+    {
+        return hbar / scale;
+    }
+}
+"""
+
+
+triangle_up = """
+float rect(vec2 pointcoord, float size)
+{
+    float height = $v_size*sqrt(3.)/2.;
+    float bottom = ((pointcoord.y - 0.5)*size - height/2.);
+    float rotated_y = sqrt(3.)/2. * (pointcoord.x - 0.5) * size
+              - 0.5 * ((pointcoord.y - 0.5)*size - height/6.) + height/6.;
+    float right_edge = (rotated_y - height/2.);
+    float cc_rotated_y = -sqrt(3.)/2. * (pointcoord.x - 0.5)*size
+              - 0.5 * ((pointcoord.y - 0.5)*size - height/6.) + height/6.;
+    float left_edge = (cc_rotated_y - height/2.);
+    float slanted_edges = max(right_edge, left_edge);
+    return max(slanted_edges, bottom);
+}
+"""
+
+triangle_down = """
+float rect(vec2 pointcoord, float size)
+{
+    float height = -$v_size*sqrt(3.)/2.;
+    float bottom = -((pointcoord.y - 0.5)*size - height/2.);
+    float rotated_y = sqrt(3.)/2. * (pointcoord.x - 0.5) * size
+                - 0.5 * ((pointcoord.y - 0.5)*size - height/6.) + height/6.;
+    float right_edge = -(rotated_y - height/2.);
+    float cc_rotated_y = -sqrt(3.)/2. * (pointcoord.x - 0.5)*size
+                - 0.5 * ((pointcoord.y - 0.5)*size - height/6.) + height/6.;
+    float left_edge = -(cc_rotated_y - height/2.);
+    float slanted_edges = max(right_edge, left_edge);
+    return max(slanted_edges, bottom);
+}
+"""
+
+
+star = """
+float rect(vec2 pointcoord, float size)
+{
+    float star = -10000.;
+    const float PI2_5 = 3.141592653589*2./5.;
+    const float PI2_20 = 3.141592653589/10.;  //PI*2/20
+    // downwards shift to that the marker center is halfway vertically
+    // between the top of the upward spike (y = -v_size/2)
+    // and the bottom of one of two downward spikes
+    // (y = +v_size/2*cos(2*pi/10) approx +v_size/2*0.8)
+    // center is at -v_size/2*0.1
+    float shift_y = -0.05*$v_size;
+    // first spike upwards,
+    // rotate spike by 72 deg four times to complete the star
+    for (int i = 0; i <= 4; i++)
+    {
+        //if not the first spike, rotate it upwards
+        float x = (pointcoord.x - 0.5)*size;
+        float y = (pointcoord.y - 0.5)*size;
+        float spike_rot_angle = i*PI2_5;
+        float cosangle = cos(spike_rot_angle);
+        float sinangle = sin(spike_rot_angle);
+        float spike_x = x;
+        float spike_y = y + shift_y;
+        if (i > 0)
+        {
+            spike_x = cosangle * x - sinangle * (y + shift_y);
+            spike_y = sinangle * x + cosangle * (y + shift_y);
+        }
+        // in the frame where the spike is upwards:
+        // rotate 18 deg the zone x < 0 around the top of the star
+        // (point whose coords are -s/2, 0 where s is the size of the marker)
+        // compute y coordonates as well because
+        // we do a second rotation to put the spike at its final position
+        float rot_center_y = -$v_size/2;
+        float rot18x = cos(PI2_20) * spike_x
+                            - sin(PI2_20) * (spike_y - rot_center_y);
+        //rotate -18 deg the zone x > 0 arount the top of the star
+        float rot_18x = cos(PI2_20) * spike_x
+                            + sin(PI2_20) * (spike_y - rot_center_y);
+        float bottom = spike_y - $v_size/10.;
+        //                     max(left edge, right edge)
+        float spike = max(bottom, max(rot18x, -rot_18x));
+        if (i == 0)
+        {// first spike, skip the rotation
+            star = spike;
+        }
+        else // i > 0
+        {
+            star = min(star, spike);
+        }
+    }
+    return star;
+}
+"""
+
+# the following two markers needs x and y sizes
+rect = """
+float rect(vec2 pointcoord, float size)
+{
+    float x_boundaries = abs(pointcoord.x - 0.5)*size - $v_size.x/2.;
+    float y_boundaries = abs(pointcoord.y - 0.5)*size - $v_size.y/2.;
+    return max(x_boundaries, y_boundaries);
+}
+"""
+
+ellipse = """
+float rect(vec2 pointcoord, float size)
+{
+    float x = (pointcoord.x - 0.5)*size;
+    float y = (pointcoord.y - 0.5)*size;
+    // normalise radial distance (for edge and antialising to remain isotropic)
+    // Scaling factor is the norm of the gradient of the function defining
+    // the surface taken at a well chosen point on the edge of the ellipse
+    // f(x, y) = (sqrt(x^2/a^2 + y^2/b^2) = 0.5 in this case
+    // where a = v_size.x and b = v_size.y)
+    // The well chosen point on the edge of the ellipse should be the point
+    // whose normal points towards the current point.
+    // Below we choose a different point whose computation
+    // is simple enough to fit here.
+    float f = length(vec2(x / $v_size.x, y / $v_size.y));
+    // We set the default value of the norm so that
+    // - near the axes (x=0 or y=0 +/- 1 pixel), the norm is correct
+    //   (the computation below is unstable near the axes)
+    // - if the ellipse is a circle, the norm is correct
+    // - if we are deep in the interior of the ellipse the norm
+    //   is set to an arbitrary value (but is not used)
+    float norm = abs(x) < 1. ? 1./$v_size.y : 1./$v_size.x;
+    if (f > 1e-3 && abs($v_size.x - $v_size.y) > 1e-3
+        && abs(x) > 1. && abs(y) > 1.)
+    {
+        // Find the point x0, y0 on the ellipse which has the same hyperbola
+        // coordinate in the elliptic coordinate system linked to the ellipse
+        // (finding the right 'well chosen' point is too complicated)
+        // Visually it's nice, even at high eccentricities, where
+        // the approximation is visible but not ugly.
+        float a = $v_size.x/2.;
+        float b = $v_size.y/2.;
+        float C = max(a, b);
+        float c = min(a, b);
+        float focal_length = sqrt(C*C - c*c);
+        float fl2 = focal_length*focal_length;
+        float x2 = x*x;
+        float y2 = y*y;
+        float tmp = fl2 + x2 + y2;
+        float x0 = 0;
+        float y0 = 0;
+        if ($v_size.x > $v_size.y)
+        {
+            float cos2v = 0.5 * (tmp - sqrt(tmp*tmp - 4.*fl2*x2)) / fl2;
+            cos2v = fract(cos2v);
+            x0 = a * sqrt(cos2v);
+            // v_size.x = focal_length*cosh m where m is the ellipse coordinate
+            y0 = b * sqrt(1-cos2v);
+            // v_size.y = focal_length*sinh m
+        }
+        else // $v_size.x < $v_size.y
+        {//exchange x and y axis for elliptic coordinate
+            float cos2v = 0.5 * (tmp - sqrt(tmp*tmp - 4.*fl2*y2)) / fl2;
+            cos2v = fract(cos2v);
+            x0 = a * sqrt(1-cos2v);
+            // v_size.x = focal_length*sinh m where m is the ellipse coordinate
+            y0 = b * sqrt(cos2v);
+            // v_size.y = focal_length*cosh m
+        }
+        vec2 normal = vec2(2.*x0/v_size.x/v_size.x, 2.*y0/v_size.y/v_size.y);
+        norm = length(normal);
+    }
+    return (f - 0.5) / norm;
+}
+"""
+
+_marker_dict = {
+    'disc': disc,
+    'arrow': arrow,
+    'ring': ring,
+    'clobber': clobber,
+    'square': square,
+    'diamond': diamond,
+    'vbar': vbar,
+    'hbar': hbar,
+    'cross': cross,
+    'tailed_arrow': tailed_arrow,
+    'x': x_,
+    'triangle_up': triangle_up,
+    'triangle_down': triangle_down,
+    'star': star,
+    # aliases
+    'o': disc,
+    '+': cross,
+    's': square,
+    '-': hbar,
+    '|': vbar,
+    '->': tailed_arrow,
+    '>': arrow,
+    '^': triangle_up,
+    'v': triangle_down,
+    '*': star
+}
+marker_types = tuple(sorted(list(_marker_dict.keys())))
+
+
+class MarkersVisual(Visual):
+    """ Visual displaying marker symbols.
+    """
+    def __init__(self):
+        self._program = ModularProgram(vert, frag)
+        self._v_size_var = Variable('varying float v_size')
+        self._program.vert['v_size'] = self._v_size_var
+        self._program.frag['v_size'] = self._v_size_var
+        self._program.vert['scalarsize'] = Function(size1d)
+        self._program.frag['scalarsize'] = Function(size1d)
+        Visual.__init__(self)
+        self.set_gl_state(depth_test=False, blend=True,
+                          blend_func=('src_alpha', 'one_minus_src_alpha'))
+
+    def set_data(self, pos=None, symbol='o', size=10., edge_width=1.,
+                 edge_width_rel=None, edge_color='black', face_color='white',
+                 scaling=False):
+        """ Set the data used to display this visual.
+
+        Parameters
+        ----------
+        pos : array
+            The array of locations to display each symbol.
+        symbol : str
+            The style of symbol to draw (see Notes).
+        size : float or array
+            The symbol size in px.
+        edge_width : float | None
+            The width of the symbol outline in pixels.
+        edge_width_rel : float | None
+            The width as a fraction of marker size. Exactly one of
+            `edge_width` and `edge_width_rel` must be supplied.
+        edge_color : Color | ColorArray
+            The color used to draw each symbol outline.
+        face_color : Color | ColorArray
+            The color used to draw each symbol interior.
+        scaling : bool
+            If set to True, marker scales when rezooming.
+
+        Notes
+        -----
+        Allowed style strings are: disc, arrow, ring, clobber, square, diamond,
+        vbar, hbar, cross, tailed_arrow, x, triangle_up, triangle_down,
+        and star.
+        """
+        assert (isinstance(pos, np.ndarray) and
+                pos.ndim == 2 and pos.shape[1] in (2, 3))
+        if (edge_width is not None) + (edge_width_rel is not None) != 1:
+            raise ValueError('exactly one of edge_width and edge_width_rel '
+                             'must be non-None')
+        if edge_width is not None:
+            if edge_width < 0:
+                raise ValueError('edge_width cannot be negative')
+        else:
+            if edge_width_rel < 0:
+                raise ValueError('edge_width_rel cannot be negative')
+        self.set_symbol(symbol)
+        self.scaling = scaling
+
+        edge_color = ColorArray(edge_color).rgba
+        if len(edge_color) == 1:
+            edge_color = edge_color[0]
+
+        face_color = ColorArray(face_color).rgba
+        if len(face_color) == 1:
+            face_color = face_color[0]
+
+        n = len(pos)
+        data = np.zeros(n, dtype=[('a_position', np.float32, 3),
+                                  ('a_fg_color', np.float32, 4),
+                                  ('a_bg_color', np.float32, 4),
+                                  ('a_size', np.float32, 1),
+                                  ('a_edgewidth', np.float32, 1)])
+        data['a_fg_color'] = edge_color
+        data['a_bg_color'] = face_color
+        if edge_width is not None:
+            data['a_edgewidth'] = edge_width
+        else:
+            data['a_edgewidth'] = size*edge_width_rel
+        data['a_position'][:, :pos.shape[1]] = pos
+        data['a_size'] = size
+        self.antialias = 1.
+        self._data = data
+        self._vbo = VertexBuffer(data)
+        self.update()
+
+    def set_symbol(self, symbol='o'):
+        """Set the symbol
+
+        Parameters
+        ----------
+        symbol : str
+            The symbol.
+        """
+        _check_valid('symbol', symbol, marker_types)
+        self._marker_fun = Function(_marker_dict[symbol])
+        self._marker_fun['v_size'] = self._v_size_var
+        self._program.frag['marker'] = self._marker_fun
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        Visual.draw(self, transforms)
+
+        xform = transforms.get_full_transform()
+        self._program.vert['transform'] = xform
+        # TO DO: find a way to avoid copying data and rebinding them to the vbo
+        if self.scaling:
+            update_data = self._data.copy()
+            # 0.5 factor due to the difference between the viewbox spanning
+            # [-1, 1] in the framebuffer coordinates intervals
+            # and the viewbox spanning [0, 1] intervals
+            # in the Visual coordinates
+            # TO DO: find a way to get the scale directly
+            scale = (
+                0.5*transforms.visual_to_document.simplified().scale[:2] *
+                transforms.document_to_framebuffer.simplified().scale[:2] *
+                transforms.framebuffer_to_render.simplified().scale[:2]
+            )
+            update_data['a_size'] *= min(scale)
+            update_data['a_edgewidth'] *= min(scale)
+            self._vbo.set_data(update_data)
+        self._program.prepare()
+        self._program['u_antialias'] = self.antialias
+
+        d2f = transforms.document_to_framebuffer
+        self._program['u_px_scale'] = (d2f.map((1, 0)) - d2f.map((0, 0)))[0]
+        self._program.bind(self._vbo)
+        self._program.draw('points')
+
+    def bounds(self, mode, axis):
+        """Get the bounds
+
+        Parameters
+        ----------
+        mode : str
+            Describes the type of boundary requested. Can be "visual", "data",
+            or "mouse".
+        axis : 0, 1, 2
+            The axis along which to measure the bounding values, in
+            x-y-z order.
+        """
+        pos = self._data['a_position']
+        if pos is None:
+            return None
+        if pos.shape[1] > axis:
+            return (pos[:, axis].min(), pos[:, axis].max())
+        else:
+            return (0, 0)
diff --git a/vispy/visuals/mesh.py b/vispy/visuals/mesh.py
new file mode 100644
index 0000000..01d4bcf
--- /dev/null
+++ b/vispy/visuals/mesh.py
@@ -0,0 +1,348 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+""" A MeshVisual Visual that uses the new shader Function.
+"""
+
+from __future__ import division
+
+import numpy as np
+
+from .visual import Visual
+from .shaders import ModularProgram, Function, Varying
+from ..gloo import VertexBuffer, IndexBuffer
+from ..geometry import MeshData
+from ..color import Color
+
+## Snippet templates (defined as string to force user to create fresh Function)
+# Consider these stored in a central location in vispy ...
+
+
+vertex_template = """
+
+void main() {
+    gl_Position = $transform($to_vec4($position));
+}
+"""
+
+fragment_template = """
+void main() {
+    gl_FragColor = $color;
+}
+"""
+
+phong_template = """
+vec4 phong_shading(vec4 color) {
+    vec4 o = $transform(vec4(0, 0, 0, 1));
+    vec4 n = $transform(vec4($normal, 1));
+    vec3 norm = normalize((n-o).xyz);
+    vec3 light = normalize($light_dir.xyz);
+    float p = dot(light, norm);
+    p = (p < 0. ? 0. : p);
+    vec4 diffuse = $light_color * p;
+    diffuse.a = 1.0;
+    p = dot(reflect(light, norm), vec3(0,0,1));
+    if (p < 0.0) {
+        p = 0.0;
+    }
+    vec4 specular = $light_color * 5.0 * pow(p, 100.);
+    return color * ($ambient + diffuse) + specular;
+}
+"""
+
+## Functions that can be used as is (don't have template variables)
+# Consider these stored in a central location in vispy ...
+
+vec3to4 = Function("""
+vec4 vec3to4(vec3 xyz) {
+    return vec4(xyz, 1.0);
+}
+""")
+
+vec2to4 = Function("""
+vec4 vec2to4(vec2 xyz) {
+    return vec4(xyz, 0.0, 1.0);
+}
+""")
+
+
+class MeshVisual(Visual):
+    """Mesh visual
+
+    Parameters
+    ----------
+    vertices : array-like | None
+        The vertices.
+    faces : array-like | None
+        The faces.
+    vertex_colors : array-like | None
+        Colors to use for each vertex.
+    face_colors : array-like | None
+        Colors to use for each face.
+    color : instance of Color
+        The color to use.
+    meshdata : instance of MeshData | None
+        The meshdata.
+    shading : str | None
+        Shading to use.
+    mode : str
+        The drawing mode.
+    **kwargs : dict
+        Keyword arguments to pass to `Visual`.
+    """
+    def __init__(self, vertices=None, faces=None, vertex_colors=None,
+                 face_colors=None, color=(0.5, 0.5, 1, 1), meshdata=None,
+                 shading=None, mode='triangles', **kwargs):
+        Visual.__init__(self, **kwargs)
+
+        self.set_gl_state('translucent', depth_test=True,
+                          cull_face=False)
+
+        # Create a program
+        self._program = ModularProgram(vertex_template, fragment_template)
+        self._program.vert['pre'] = ''
+        self._program.vert['post'] = ''
+        self._program.frag['pre'] = ''
+        self._program.frag['post'] = ''
+
+        # Define buffers
+        self._vertices = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
+        self._normals = None
+        self._faces = IndexBuffer()
+        self._colors = VertexBuffer(np.zeros((0, 4), dtype=np.float32))
+        self._normals = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
+
+        # Whether to use _faces index
+        self._indexed = None
+
+        # Uniform color
+        self._color = None
+
+        # primitive mode
+        self._mode = mode
+
+        # varyings
+        self._color_var = Varying('v_color', dtype='vec4')
+        self._normal_var = Varying('v_normal', dtype='vec3')
+
+        # Function for computing phong shading
+        self._phong = Function(phong_template)
+
+        # Init
+        self.shading = shading
+        self._bounds = None
+        # Note we do not call subclass set_data -- often the signatures
+        # do no match.
+        MeshVisual.set_data(self, vertices=vertices, faces=faces,
+                            vertex_colors=vertex_colors,
+                            face_colors=face_colors, meshdata=meshdata,
+                            color=color)
+
+    def set_data(self, vertices=None, faces=None, vertex_colors=None,
+                 face_colors=None, color=None, meshdata=None):
+        """Set the mesh data
+
+        Parameters
+        ----------
+        vertices : array-like | None
+            The vertices.
+        faces : array-like | None
+            The faces.
+        vertex_colors : array-like | None
+            Colors to use for each vertex.
+        face_colors : array-like | None
+            Colors to use for each face.
+        color : instance of Color
+            The color to use.
+        meshdata : instance of MeshData | None
+            The meshdata.
+        """
+        if meshdata is not None:
+            self._meshdata = meshdata
+        else:
+            self._meshdata = MeshData(vertices=vertices, faces=faces,
+                                      vertex_colors=vertex_colors,
+                                      face_colors=face_colors)
+        self._bounds = self._meshdata.get_bounds()
+        if color is not None:
+            self._color = Color(color)
+        self.mesh_data_changed()
+
+    @property
+    def mode(self):
+        """The triangle mode used to draw this mesh.
+
+        Options are:
+
+            * 'triangles': Draw one triangle for every three vertices
+              (eg, [1,2,3], [4,5,6], [7,8,9)
+            * 'triangle_strip': Draw one strip for every vertex excluding the
+              first two (eg, [1,2,3], [2,3,4], [3,4,5])
+            * 'triangle_fan': Draw each triangle from the first vertex and the
+              last two vertices (eg, [1,2,3], [1,3,4], [1,4,5])
+        """
+        return self._mode
+
+    @mode.setter
+    def mode(self, m):
+        modes = ['triangles', 'triangle_strip', 'triangle_fan']
+        if m not in modes:
+            raise ValueError("Mesh mode must be one of %s" % ', '.join(modes))
+        self._mode = m
+
+    @property
+    def mesh_data(self):
+        """The mesh data"""
+        return self._meshdata
+
+    @property
+    def color(self):
+        """The uniform color for this mesh.
+
+        This value is only used if per-vertex or per-face colors are not
+        specified.
+        """
+        return self._color
+
+    @color.setter
+    def color(self, c):
+        self.set_data(color=c)
+
+    def mesh_data_changed(self):
+        self._data_changed = True
+        self.update()
+
+    def _update_data(self):
+        md = self.mesh_data
+        # Update vertex/index buffers
+        if self.shading == 'smooth' and not md.has_face_indexed_data():
+            v = md.get_vertices()
+            if v is None:
+                return False
+            if v.shape[-1] == 2:
+                v = np.concatenate((v, np.zeros((v.shape[:-1] + (1,)))), -1)
+            self._vertices.set_data(v, convert=True)
+            self._normals.set_data(md.get_vertex_normals(), convert=True)
+            self._faces.set_data(md.get_faces(), convert=True)
+            self._indexed = True
+            if md.has_vertex_color():
+                self._colors.set_data(md.get_vertex_colors(), convert=True)
+            elif md.has_face_color():
+                self._colors.set_data(md.get_face_colors(), convert=True)
+            else:
+                self._colors.set_data(np.zeros((0, 4), dtype=np.float32))
+        else:
+            v = md.get_vertices(indexed='faces')
+            if v is None:
+                return False
+            if v.shape[-1] == 2:
+                v = np.concatenate((v, np.zeros((v.shape[:-1] + (1,)))), -1)
+            self._vertices.set_data(v, convert=True)
+            if self.shading == 'smooth':
+                normals = md.get_vertex_normals(indexed='faces')
+                self._normals.set_data(normals, convert=True)
+            elif self.shading == 'flat':
+                normals = md.get_face_normals(indexed='faces')
+                self._normals.set_data(normals, convert=True)
+            else:
+                self._normals.set_data(np.zeros((0, 3), dtype=np.float32))
+            self._indexed = False
+            if md.has_vertex_color():
+                self._colors.set_data(md.get_vertex_colors(indexed='faces'),
+                                      convert=True)
+            elif md.has_face_color():
+                self._colors.set_data(md.get_face_colors(indexed='faces'),
+                                      convert=True)
+            else:
+                self._colors.set_data(np.zeros((0, 4), dtype=np.float32))
+        self._program.vert['position'] = self._vertices
+
+        # Position input handling
+        if v.shape[-1] == 2:
+            self._program.vert['to_vec4'] = vec2to4
+        elif v.shape[-1] == 3:
+            self._program.vert['to_vec4'] = vec3to4
+        else:
+            raise TypeError("Vertex data must have shape (...,2) or (...,3).")
+
+        # Color input handling
+        colors = self._colors if self._colors.size > 0 else self._color.rgba
+        self._program.vert[self._color_var] = colors
+
+        # Shading
+        if self.shading is None:
+            self._program.frag['color'] = self._color_var
+        else:
+            # Normal data comes via vertex shader
+            if self._normals.size > 0:
+                normals = self._normals
+            else:
+                normals = (1., 0., 0.)
+
+            self._program.vert[self._normal_var] = normals
+            self._phong['normal'] = self._normal_var
+
+            # Additional phong properties
+            self._phong['light_dir'] = (1.0, 1.0, 5.0)
+            self._phong['light_color'] = (1.0, 1.0, 1.0, 1.0)
+            self._phong['ambient'] = (0.3, 0.3, 0.3, 1.0)
+
+            self._program.frag['color'] = self._phong(self._color_var)
+
+        self._data_changed = False
+
+    @property
+    def shading(self):
+        """ The shading method used.
+        """
+        return self._shading
+
+    @shading.setter
+    def shading(self, value):
+        assert value in (None, 'flat', 'smooth')
+        self._shading = value
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        if self._data_changed:
+            if self._update_data() is False:
+                return
+            self._data_changed = False
+
+        Visual.draw(self, transforms)
+
+        full_tr = transforms.get_full_transform()
+        self._program.vert['transform'] = full_tr
+        doc_tr = transforms.visual_to_document
+        self._phong['transform'] = doc_tr
+
+        # Draw
+        if self._indexed:
+            self._program.draw(self._mode, self._faces)
+        else:
+            self._program.draw(self._mode)
+
+    def bounds(self, mode, axis):
+        """Get the bounds
+
+        Parameters
+        ----------
+        mode : str
+            Describes the type of boundary requested. Can be "visual", "data",
+            or "mouse".
+        axis : 0, 1, 2
+            The axis along which to measure the bounding values, in
+            x-y-z order.
+        """
+        if self._bounds is None:
+            return None
+        return self._bounds[axis]
diff --git a/vispy/visuals/polygon.py b/vispy/visuals/polygon.py
new file mode 100644
index 0000000..b149248
--- /dev/null
+++ b/vispy/visuals/polygon.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+
+"""
+Simple polygon visual based on MeshVisual and LineVisual
+"""
+
+from __future__ import division
+
+import numpy as np
+
+from .. import gloo
+from .visual import Visual
+from .mesh import MeshVisual
+from .line import LineVisual
+from ..color import Color
+from ..geometry import PolygonData
+
+
+class PolygonVisual(Visual):
+    """
+    Displays a 2D polygon
+
+    Parameters
+    ----------
+    pos : array
+        Set of vertices defining the polygon.
+    color : str | tuple | list of colors
+        Fill color of the polygon.
+    border_color : str | tuple | list of colors
+        Border color of the polygon.
+    border_width : int
+        Border width in pixels.
+    **kwargs : dict
+        Keyword arguments to pass to `PolygonVisual`.
+    """
+    def __init__(self, pos=None, color='black',
+                 border_color=None, border_width=1, **kwargs):
+        super(PolygonVisual, self).__init__(**kwargs)
+
+        self.mesh = MeshVisual()
+        self.border = LineVisual()
+        self._pos = pos
+        self._color = Color(color)
+        self._border_width = border_width
+        self._border_color = Color(border_color)
+        self._update()
+        #glopts = kwargs.pop('gl_options', 'translucent')
+        #self.set_gl_options(glopts)
+
+    @property
+    def pos(self):
+        """ The vertex position of the polygon.
+        """
+        return self._pos
+
+    @pos.setter
+    def pos(self, pos):
+        self._pos = pos
+        self._update()
+
+    @property
+    def color(self):
+        """ The color of the polygon.
+        """
+        return self._color
+
+    @color.setter
+    def color(self, color):
+        self._color = Color(color, clip=True)
+        self._update()
+
+    @property
+    def border_color(self):
+        """ The border color of the polygon.
+        """
+        return self._border_color
+
+    @border_color.setter
+    def border_color(self, border_color):
+        self._border_color = Color(border_color)
+        self._update()
+
+    def _update(self):
+        self.data = PolygonData(vertices=np.array(self._pos, dtype=np.float32))
+        if self._pos is None:
+            return
+        if not self._color.is_blank:
+            pts, tris = self.data.triangulate()
+            self.mesh.set_data(vertices=pts, faces=tris.astype(np.uint32),
+                               color=self._color.rgba)
+        if not self._border_color.is_blank:
+            # Close border if it is not already.
+            border_pos = self._pos
+            if np.any(border_pos[0] != border_pos[1]):
+                border_pos = np.concatenate([border_pos, border_pos[:1]], 
+                                            axis=0)
+            self.border.set_data(pos=border_pos,
+                                 color=self._border_color.rgba, 
+                                 width=self._border_width,
+                                 connect='strip')
+        self.update()
+
+    def set_gl_options(self, *args, **kwargs):
+        self.mesh.set_gl_options(*args, **kwargs)
+
+    def update_gl_options(self, *args, **kwargs):
+        self.mesh.update_gl_options(*args, **kwargs)
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        if self._pos is None:
+            return
+        if not self._color.is_blank:
+            gloo.set_state(polygon_offset_fill=True, 
+                           cull_face=False)
+            gloo.set_polygon_offset(1, 1)
+            self.mesh.draw(transforms)
+        if not self._border_color.is_blank:
+            self.border.draw(transforms)
diff --git a/vispy/scene/visuals/rectangle.py b/vispy/visuals/rectangle.py
similarity index 85%
rename from vispy/scene/visuals/rectangle.py
rename to vispy/visuals/rectangle.py
index ab6aa05..f7e358c 100644
--- a/vispy/scene/visuals/rectangle.py
+++ b/vispy/visuals/rectangle.py
@@ -10,19 +10,22 @@ Simple ellipse visual based on PolygonVisual
 from __future__ import division
 
 import numpy as np
-from ...color import Color
-from .polygon import Polygon, Mesh, Line
+from ..color import Color
+from .polygon import PolygonVisual
 
 
-class Rectangle(Polygon):
+class RectangleVisual(PolygonVisual):
     """
     Displays a 2D rectangle with optional rounded corners
 
     Parameters
     ----------
-
     pos :  array
         Center of the rectangle
+    color : instance of Color
+        The fill color to use.
+    border_color : instance of Color
+        The border color to use.
     height : float
         Length of the rectangle along y-axis
         Defaults to 1.0
@@ -34,8 +37,9 @@ class Rectangle(Polygon):
         Defaults to 0.
     """
     def __init__(self, pos=None, color='black', border_color=None,
-                 height=1.0, width=1.0, radius=[0., 0., 0., 0.], **kwds):
-        super(Rectangle, self).__init__()
+                 height=1.0, width=1.0, radius=[0., 0., 0., 0.], **kwargs):
+        super(RectangleVisual, self).__init__()
+        self.mesh.mode = 'triangle_fan'
         self._vertices = None
         self._pos = pos
         self._color = Color(color)
@@ -160,13 +164,16 @@ class Rectangle(Polygon):
         self._update()
 
     def _update(self):
-        if self._pos is not None:
-            self._generate_vertices(pos=self._pos, radius=self._radius,
-                                    height=self._height, width=self._width,
-                                    )
-            self.mesh = Mesh(vertices=self._vertices, color=self._color.rgba,
-                             mode='triangle_fan')
-            if not self._border_color.is_blank():
-                self.border = Line(pos=self._vertices[1:, ..., :2],
-                                   color=self._border_color.rgba)
-        #self.update()
+        if self._pos is None:
+            return
+        self._generate_vertices(pos=self._pos, radius=self._radius,
+                                height=self._height, width=self._width)
+        
+        if not self._color.is_blank:
+            self.mesh.set_data(vertices=self._vertices, 
+                               color=self._color.rgba)
+        if not self._border_color.is_blank:
+            self.border.set_data(pos=self._vertices[1:, ..., :2],
+                                 color=self._border_color.rgba)
+
+        self.update()
diff --git a/vispy/scene/visuals/regular_polygon.py b/vispy/visuals/regular_polygon.py
similarity index 52%
rename from vispy/scene/visuals/regular_polygon.py
rename to vispy/visuals/regular_polygon.py
index aefc40c..7b7de30 100644
--- a/vispy/scene/visuals/regular_polygon.py
+++ b/vispy/visuals/regular_polygon.py
@@ -1,19 +1,19 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 
 """
-RegularPolygon visual based on EllipseVisual
+RegularPolygonVisual visual based on EllipseVisual
 """
 
 from __future__ import division
 
-from ...color import Color
-from .ellipse import Ellipse, Mesh, Line
+from ..color import Color
+from .ellipse import EllipseVisual
 
 
-class RegularPolygon(Ellipse):
+class RegularPolygonVisual(EllipseVisual):
     """
     Displays a regular polygon
 
@@ -22,19 +22,20 @@ class RegularPolygon(Ellipse):
 
     pos : array
         Center of the regular polygon
+    color : str | tuple | list of colors
+        Fill color of the polygon
+    border_color : str | tuple | list of colors
+        Border color of the polygon
     radius : float
         Radius of the regular polygon
         Defaults to  0.1
     sides : int
         Number of sides of the regular polygon
-    color : str | tuple | list of colors
-        Fill color of the polygon
-    border_color : str | tuple | list of colors
-        Border color of the polygon
     """
     def __init__(self, pos=None, color='black', border_color=None,
-                 radius=0.1, sides=4, **kwds):
-        super(Ellipse, self).__init__()
+                 radius=0.1, sides=4, **kwargs):
+        super(EllipseVisual, self).__init__()
+        self.mesh.mode = 'triangle_fan'
         self._pos = pos
         self._color = Color(color)
         self._border_color = Color(border_color)
@@ -51,19 +52,24 @@ class RegularPolygon(Ellipse):
     @sides.setter
     def sides(self, sides):
         if sides < 3:
-            raise ValueError('Polygon must have at least 3 sides, not %s'
+            raise ValueError('PolygonVisual must have at least 3 sides, not %s'
                              % sides)
         self._sides = sides
         self._update()
 
     def _update(self):
-        if self._pos is not None:
-            self._generate_vertices(pos=self._pos, radius=self._radius,
-                                    start_angle=0.,
-                                    span_angle=360.,
-                                    num_segments=self._sides)
-            self.mesh = Mesh(vertices=self._vertices, color=self._color.rgba,
-                             mode='triangle_fan')
-            if not self._border_color.is_blank():
-                self.border = Line(pos=self._vertices[1:],
-                                   color=self._border_color.rgba)
+        if self._pos is None:
+            return
+        self._generate_vertices(pos=self._pos, radius=self._radius,
+                                start_angle=0.,
+                                span_angle=360.,
+                                num_segments=self._sides)
+        
+        if not self._color.is_blank:
+            self.mesh.set_data(vertices=self._vertices, 
+                               color=self._color.rgba)
+        if not self._border_color.is_blank:
+            self.border.set_data(pos=self._vertices[1:],
+                                 color=self._border_color.rgba)
+
+        self.update()
diff --git a/vispy/scene/shaders/__init__.py b/vispy/visuals/shaders/__init__.py
similarity index 65%
rename from vispy/scene/shaders/__init__.py
rename to vispy/visuals/shaders/__init__.py
index 6ffb2df..53bd0f2 100644
--- a/vispy/scene/shaders/__init__.py
+++ b/vispy/visuals/shaders/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 Provides functionality for composing shaders from multiple GLSL
@@ -10,6 +10,7 @@ __all__ = ['ModularProgram', 'Function', 'MainFunction', 'Variable', 'Varying',
            'FunctionChain', 'Compiler']
 
 from .program import ModularProgram  # noqa
-from .function import (Function, MainFunction, Variable, Varying,  # noqa
-                       FunctionChain)  # noqa
+from .function import Function, MainFunction, FunctionChain  # noqa
+from .function import StatementList  # noqa
+from .variable import Variable, Varying  # noqa
 from .compiler import Compiler  # noqa
diff --git a/vispy/scene/shaders/compiler.py b/vispy/visuals/shaders/compiler.py
similarity index 91%
rename from vispy/scene/shaders/compiler.py
rename to vispy/visuals/shaders/compiler.py
index 1fa1dd3..f84eebf 100644
--- a/vispy/scene/shaders/compiler.py
+++ b/vispy/visuals/shaders/compiler.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
@@ -11,27 +11,27 @@ from ... import gloo
 
 class Compiler(object):
     """
-    Compiler is used to convert Function and Variable instances into 
+    Compiler is used to convert Function and Variable instances into
     ready-to-use GLSL code. This class handles name mangling to ensure that
     there are no name collisions amongst global objects. The final name of
     each object may be retrieved using ``Compiler.__getitem__(obj)``.
-    
+
     Accepts multiple root Functions as keyword arguments. ``compile()`` then
     returns a dict of GLSL strings with the same keys.
-    
+
     Example::
-    
+
         # initialize with two main functions
         compiler = Compiler(vert=v_func, frag=f_func)
-        
+
         # compile and extract shaders
         code = compiler.compile()
         v_code = code['vert']
         f_code = code['frag']
-        
+
         # look up name of some object
         name = compiler[obj]
-    
+
     """
     def __init__(self, **shaders):
         # cache of compilation results for each function and variable
@@ -40,38 +40,38 @@ class Compiler(object):
 
     def __getitem__(self, item):
         """
-        Return the name of the specified object, if it has been assigned one.        
+        Return the name of the specified object, if it has been assigned one.
         """
         return self._object_names[item]
 
     def compile(self, pretty=True):
-        """ Compile all code and return a dict {name: code} where the keys 
+        """ Compile all code and return a dict {name: code} where the keys
         are determined by the keyword arguments passed to __init__().
-        
+
         Parameters
         ----------
         pretty : bool
             If True, use a slower method to mangle object names. This produces
             GLSL that is more readable.
-            If False, then the output is mostly unreadable GLSL, but is about 
+            If False, then the output is mostly unreadable GLSL, but is about
             10x faster to compile.
-        
+
         """
         # Authoritative mapping of {obj: name}
         self._object_names = {}
-        
+
         #
         # 1. collect list of dependencies for each shader
         #
-        
+
         # maps {shader_name: [deps]}
         self._shader_deps = {}
-        
+
         for shader_name, shader in self.shaders.items():
             this_shader_deps = []
             self._shader_deps[shader_name] = this_shader_deps
             dep_set = set()
-            
+
             for dep in shader.dependencies(sort=True):
                 # visit each object no more than once per shader
                 if dep.name is None or dep in dep_set:
@@ -86,21 +86,20 @@ class Compiler(object):
             self._rename_objects_pretty()
         else:
             self._rename_objects_fast()
-        
+
         #
         # 3. Now we have a complete namespace; concatenate all definitions
         # together in topological order.
         #
         compiled = {}
         obj_names = self._object_names
-        
+
         for shader_name, shader in self.shaders.items():
-            code = ['// Generated code by function composition', 
-                    '#version 120', '']
+            code = []
             for dep in self._shader_deps[shader_name]:
                 dep_code = dep.definition(obj_names)
                 if dep_code is not None:
-                    # strip out version pragma if present; 
+                    # strip out version pragma if present;
                     regex = r'#version (\d+)'
                     m = re.search(regex, dep_code)
                     if m is not None:
@@ -110,17 +109,17 @@ class Compiler(object):
                                                "120 is supported.")
                         dep_code = re.sub(regex, '', dep_code)
                     code.append(dep_code)
-                
+
             compiled[shader_name] = '\n'.join(code)
-            
+
         self.code = compiled
         return compiled
 
     def _rename_objects_fast(self):
-        """ Rename all objects quickly to guaranteed-unique names using the 
+        """ Rename all objects quickly to guaranteed-unique names using the
         id() of each object.
-        
-        This produces mostly unreadable GLSL, but is about 10x faster to 
+
+        This produces mostly unreadable GLSL, but is about 10x faster to
         compile.
         """
         for shader_name, deps in self._shader_deps.items():
@@ -130,18 +129,18 @@ class Compiler(object):
                     ext = '_%x' % id(dep)
                     name = name[:32-len(ext)] + ext
                 self._object_names[dep] = name
-            
+
     def _rename_objects_pretty(self):
         """ Rename all objects like "name_1" to avoid conflicts. Objects are
-        only renamed if necessary. 
-        
+        only renamed if necessary.
+
         This method produces more readable GLSL, but is rather slow.
         """
         #
         # 1. For each object, add its static names to the global namespace
         #    and make a list of the shaders used by the object.
         #
-        
+
         # {name: obj} mapping for finding unique names
         # initialize with reserved keywords.
         self._global_ns = dict([(kwd, None) for kwd in gloo.util.KEYWORDS])
@@ -150,15 +149,15 @@ class Compiler(object):
 
         # for each object, keep a list of shaders the object appears in
         obj_shaders = {}
-        
+
         for shader_name, deps in self._shader_deps.items():
             for dep in deps:
                 # Add static names to namespace
                 for name in dep.static_names():
                     self._global_ns[name] = None
-                    
+
                 obj_shaders.setdefault(dep, []).append(shader_name)
-                
+
         #
         # 2. Assign new object names
         #
@@ -178,18 +177,18 @@ class Compiler(object):
                     if self._name_available(obj, new_name, shaders):
                         self._assign_name(obj, new_name, shaders)
                         break
-        
+
     def _is_global(self, obj):
-        """ Return True if *obj* should be declared in the global namespace. 
-        
+        """ Return True if *obj* should be declared in the global namespace.
+
         Some objects need to be declared only in per-shader namespaces:
         functions, static variables, and const variables may all be given
         different definitions in each shader.
         """
-        # todo: right now we assume all Variables are global, and all 
+        # todo: right now we assume all Variables are global, and all
         # Functions are local. Is this actually correct? Are there any
         # global functions? Are there any local variables?
-        from .function import Variable
+        from .variable import Variable
         return isinstance(obj, Variable)
 
     def _name_available(self, obj, name, shaders):
diff --git a/vispy/visuals/shaders/expression.py b/vispy/visuals/shaders/expression.py
new file mode 100644
index 0000000..15853a4
--- /dev/null
+++ b/vispy/visuals/shaders/expression.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+from ...ext.six import string_types
+from .shader_object import ShaderObject
+
+
+class Expression(ShaderObject):
+    """ Base class for expressions (ShaderObjects that do not have a
+    definition nor dependencies)
+    """
+    
+    def definition(self, names):
+        # expressions are declared inline.
+        return None
+
+
+class TextExpression(Expression):
+    """ Plain GLSL text to insert inline
+    """
+    
+    def __init__(self, text):
+        super(TextExpression, self).__init__()
+        if not isinstance(text, string_types):
+            raise TypeError("Argument must be string.")
+        self._text = text
+    
+    def __repr__(self):
+        return '<TextExpression %r for at 0x%x>' % (self.text, id(self))
+    
+    def expression(self, names=None):
+        return self._text
+    
+    @property
+    def text(self):
+        return self._text
+    
+    @text.setter
+    def text(self, t):
+        self._text = t
+        self.changed()
+
+    def __eq__(self, a):
+        if isinstance(a, TextExpression):
+            return a._text == self._text
+        elif isinstance(a, string_types):
+            return a == self._text
+        else:
+            return False
+    
+    def __hash__(self):
+        return self._text.__hash__()
+
+
+class FunctionCall(Expression):
+    """ Representation of a call to a function
+    
+    Essentially this is container for a Function along with its signature. 
+    """
+    def __init__(self, function, args):
+        from .function import Function
+        super(FunctionCall, self).__init__()
+        
+        if not isinstance(function, Function):
+            raise TypeError('FunctionCall needs a Function')
+        
+        sig_len = len(function.args)
+        if len(args) != sig_len:
+            raise TypeError('Function %s requires %d arguments (got %d)' %
+                            (function.name, sig_len, len(args)))
+        
+        # Ensure all expressions
+        sig = function.args
+        
+        self._function = function
+        
+        # Convert all arguments to ShaderObject, using arg name if possible.
+        self._args = [ShaderObject.create(arg, ref=sig[i][1]) 
+                      for i, arg in enumerate(args)]
+        
+        self._add_dep(function)
+        for arg in self._args:
+            self._add_dep(arg)
+    
+    def __repr__(self):
+        return '<FunctionCall of %r at 0x%x>' % (self.function.name, id(self))
+    
+    @property
+    def function(self):
+        return self._function
+    
+    @property
+    def dtype(self):
+        return self._function.rtype
+    
+    def expression(self, names):
+        str_args = [arg.expression(names) for arg in self._args]
+        args = ', '.join(str_args)
+        fname = self.function.expression(names)
+        return '%s(%s)' % (fname, args)
diff --git a/vispy/scene/shaders/function.py b/vispy/visuals/shaders/function.py
similarity index 57%
rename from vispy/scene/shaders/function.py
rename to vispy/visuals/shaders/function.py
index 23ee136..e23e6b8 100644
--- a/vispy/scene/shaders/function.py
+++ b/vispy/visuals/shaders/function.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 Classses representing GLSL objects (functions, variables, etc) that may be
@@ -18,163 +18,17 @@ trigger a recompile.
 """
 
 import re
+import logging
 import numpy as np
 
-from ...util.event import EventEmitter, Event
 from ...util.eq import eq
 from ...util import logger
 from ...ext.ordereddict import OrderedDict
 from ...ext.six import string_types
 from . import parsing
-from .compiler import Compiler
-
-VARIABLE_TYPES = ('const', 'uniform', 'attribute', 'varying', 'inout')
-
-
-class ShaderChangeEvent(Event):
-    def __init__(self, code_changed=False, value_changed=False, **kwds):
-        Event.__init__(self, type='shader_change', **kwds)
-        self.code_changed = code_changed
-        self.value_changed = value_changed
-
-
-class ShaderObject(object):
-    """ Base class for all objects that may be included in a GLSL program
-    (Functions, Variables, Expressions).
-    
-    Shader objects have a *definition* that defines the object in GLSL, an 
-    *expression* that is used to reference the object, and a set of 
-    *dependencies* that must be declared before the object is used.
-    
-    Dependencies are tracked hierarchically such that changes to any object
-    will be propagated up the dependency hierarchy to trigger a recompile.
-    """
-    
-    @classmethod
-    def create(self, obj, ref=None):
-        """ Convert *obj* to a new ShaderObject. If the output is a Variable
-        with no name, then set its name using *ref*. 
-        """
-        if isinstance(ref, Variable):
-            ref = ref.name
-        elif isinstance(ref, string_types) and ref.startswith('gl_'):
-            # gl_ names not allowed for variables
-            ref = ref[3:].lower()
-        
-        if isinstance(obj, ShaderObject):
-            if isinstance(obj, Variable) and obj.name is None:
-                obj.name = ref
-        elif isinstance(obj, string_types):
-            obj = TextExpression(obj)
-        else:
-            obj = Variable(ref, obj)
-            # Try prepending the name to indicate attribute, uniform, varying
-            if obj.vtype and obj.vtype[0] in 'auv':
-                obj.name = obj.vtype[0] + '_' + obj.name 
-        
-        return obj
-    
-    def __init__(self):
-        # emitted when any part of the code for this object has changed,
-        # including dependencies.
-        self.changed = EventEmitter(source=self, event_class=ShaderChangeEvent)
-        
-        # objects that must be declared before this object's definition.
-        # {obj: refcount}
-        self._deps = OrderedDict()  # OrderedDict for consistent code output
-    
-    @property
-    def name(self):
-        """ The name of this shader object.
-        """
-        return None
-        
-    def definition(self, obj_names):
-        """ Return the GLSL definition for this object. Use *obj_names* to
-        determine the names of dependencies.
-        """
-        return None
-    
-    def expression(self, obj_names):
-        """ Return the GLSL expression used to reference this object inline.
-        """
-        return obj_names[self]
-    
-    def dependencies(self, sort=False):
-        """ Return all dependencies required to use this object. The last item 
-        in the list is *self*.
-        """
-        alldeps = []
-        if sort:
-            def key(obj):
-                # sort deps such that we get functions, variables, self.
-                if not isinstance(obj, Variable):
-                    return (0, 0)
-                else:
-                    return (1, obj.vtype)
-            
-            deps = sorted(self._deps, key=key)
-        else:
-            deps = self._deps
-        
-        for dep in deps:
-            alldeps.extend(dep.dependencies(sort=sort))
-        alldeps.append(self)
-        return alldeps
-
-    def static_names(self):
-        """ Return a list of names that are declared in this object's 
-        definition (not including the name of the object itself).
-        
-        These names will be reserved by the compiler when automatically 
-        determining object names.
-        """
-        return []
-    
-    def _add_dep(self, dep):
-        """ Increment the reference count for *dep*. If this is a new 
-        dependency, then connect to its *changed* event.
-        """
-        if dep in self._deps:
-            self._deps[dep] += 1
-        else:
-            self._deps[dep] = 1
-            dep.changed.connect(self._dep_changed)
-
-    def _remove_dep(self, dep):
-        """ Decrement the reference count for *dep*. If the reference count 
-        reaches 0, then the dependency is removed and its *changed* event is
-        disconnected.
-        """
-        refcount = self._deps[dep]
-        if refcount == 1:
-            self._deps.pop(dep)
-            dep.changed.disconnect(self._dep_changed)
-        else:
-            self._deps[dep] -= 1
-        
-    def _dep_changed(self, event):
-        """ Called when a dependency's expression has changed.
-        """
-        logger.debug("ShaderObject changed: %r" % event.source)
-        self.changed(event)
-    
-    def compile(self):
-        """ Return a compilation of this object and its dependencies. 
-        
-        Note: this is mainly for debugging purposes; the names in this code
-        are not guaranteed to match names in any other compilations. Use
-        Compiler directly to ensure consistent naming across multiple objects. 
-        """
-        compiler = Compiler(obj=self)
-        return compiler.compile()['obj']
-    
-    def __repr__(self):
-        if self.name is not None:
-            return '<%s "%s" at 0x%x>' % (self.__class__.__name__, 
-                                          self.name, id(self))
-        else:
-            return '<%s at 0x%x>' % (self.__class__.__name__, id(self))
+from .shader_object import ShaderObject
+from .variable import Variable, Varying
+from .expression import TextExpression, FunctionCall
 
 
 class Function(ShaderObject):
@@ -192,6 +46,9 @@ class Function(ShaderObject):
     * float, int, tuple are automatically turned into a uniform Variable
     * a VertexBuffer is automatically turned into an attribute Variable
     
+    All functions have implicit "$pre" and "$post" placeholders that may be
+    used to insert code at the beginning and end of the function.
+    
     Examples
     --------
     This example shows the basic usage of the Function class::
@@ -333,7 +190,7 @@ class Function(ShaderObject):
         self._replacements = OrderedDict()
         
         # Stuff to do at the end
-        self._post_hooks = OrderedDict()
+        self._assignments = OrderedDict()
         
         # Create static Variable instances for any global variables declared
         # in the code
@@ -353,7 +210,7 @@ class Function(ShaderObject):
                 if self.name != 'main':
                     raise Exception("Varying assignment only alowed in 'main' "
                                     "function.")
-                storage = self._post_hooks
+                storage = self._assignments
             else:
                 raise TypeError("Variable assignment only allowed for "
                                 "varyings, not %s (in %s)"
@@ -361,8 +218,8 @@ class Function(ShaderObject):
         elif isinstance(key, string_types):
             if any(map(key.startswith, 
                        ('gl_PointSize', 'gl_Position', 'gl_FragColor'))):
-                storage = self._post_hooks
-            elif key in self.template_vars:
+                storage = self._assignments
+            elif key in self.template_vars or key in ('pre', 'post'):
                 storage = self._expressions
             else:
                 raise KeyError('Invalid template variable %r' % key)
@@ -382,16 +239,18 @@ class Function(ShaderObject):
             # try just updating its value.
             variable = storage.get(key, None)
             if isinstance(variable, Variable):
-                try:
+                if np.any(variable.value != val):
                     variable.value = val
-                    return
-                except Exception:
-                    # Setting value on existing Variable failed for some
-                    # reason; will need to create a new Variable instead. 
-                    pass
-        
-        #print("SET: %s[%s] = %s => %s" % 
-        #     (self, key, storage.get(key, None), val))
+                    self.changed(value_changed=True)
+                return
+            
+            # Could not set variable.value directly; instead we will need
+            # to create a new ShaderObject
+            val = ShaderObject.create(val, ref=key)
+            if variable is val:
+                # This can happen if ShaderObject.create returns the same 
+                # object (such as when setting a Transform).
+                return
         
         # Remove old references, if any
         oldval = storage.pop(key, None)
@@ -402,7 +261,6 @@ class Function(ShaderObject):
 
         # Add new references
         if val is not None:
-            val = ShaderObject.create(val, ref=key)
             if isinstance(key, Varying):
                 # tell this varying to inherit properties from 
                 # its source attribute / expression.
@@ -421,6 +279,12 @@ class Function(ShaderObject):
                     self.template_vars.add(var.lstrip('$'))
         
         self.changed(code_changed=True, value_changed=True)
+        if logger.level <= logging.DEBUG:
+            import traceback
+            last = traceback.format_list(traceback.extract_stack()[-2:-1])
+            logger.debug("Assignment would trigger shader recompile:\n"
+                         "Original:\n%r\nReplacement:\n%r\nSource:\n%s", 
+                         oldval, val, ''.join(last))
     
     def __getitem__(self, key):
         """ Return a reference to a program variable from this function.
@@ -440,7 +304,7 @@ class Function(ShaderObject):
             pass
         
         try:
-            return self._post_hooks[key]
+            return self._assignments[key]
         except KeyError:
             pass
         
@@ -537,6 +401,9 @@ class Function(ShaderObject):
             var = var.lstrip('$')
             if var == self.name:
                 continue
+            if var in ('pre', 'post'):
+                raise ValueError('GLSL uses reserved template variable $%s' % 
+                                 var)
             template_vars.add(var)
         return template_vars
     
@@ -546,17 +413,18 @@ class Function(ShaderObject):
         code = self._code
         
         # Modify name
-        code = code.replace(" " + self.name + "(", " " + names[self] + "(")
+        fname = names[self]
+        code = code.replace(" " + self.name + "(", " " + fname + "(")
 
         # Apply string replacements first -- these may contain $placeholders
         for key, val in self._replacements.items():
             code = code.replace(key, val)
         
-        # Apply post-hooks
+        # Apply assignments to the end of the function
         
         # Collect post lines
         post_lines = []
-        for key, val in self._post_hooks.items():
+        for key, val in self._assignments.items():
             if isinstance(key, Variable):
                 key = names[key]
             if isinstance(val, ShaderObject):
@@ -564,12 +432,25 @@ class Function(ShaderObject):
             line = '    %s = %s;' % (key, val)
             post_lines.append(line)
             
+        # Add a default $post placeholder if needed
+        if 'post' in self._expressions:
+            post_lines.append('    $post')
+            
         # Apply placeholders for hooks
         post_text = '\n'.join(post_lines)
         if post_text:
             post_text = '\n' + post_text + '\n'
         code = code.rpartition('}')
         code = code[0] + post_text + code[1] + code[2]
+
+        # Add a default $pre placeholder if needed
+        if 'pre' in self._expressions:
+            m = re.search(fname + r'\s*\([^{]*\)\s*{', code)
+            if m is None:
+                raise RuntimeError("Cound not find beginning of function '%s'" 
+                                   % fname) 
+            ind = m.span()[1]
+            code = code[:ind] + "\n    $pre\n" + code[ind:]
         
         # Apply template variables
         for key, val in self._expressions.items():
@@ -581,7 +462,7 @@ class Function(ShaderObject):
         if '$' in code:
             v = parsing.find_template_variables(code)
             logger.warning('Unsubstituted placeholders in code: %s\n'
-                           '  replacements made: %s' % 
+                           '  replacements made: %s', 
                            (v, list(self._expressions.keys())))
         
         return code + '\n'
@@ -608,7 +489,11 @@ class Function(ShaderObject):
         return code
 
     def __repr__(self):
-        args = ', '.join([' '.join(arg) for arg in self.args])
+        try:
+            args = ', '.join([' '.join(arg) for arg in self.args])
+        except Exception:
+            return ('<%s (error parsing signature) at 0x%x>' % 
+                    (self.__class__.__name__, id(self)))
         return '<%s "%s %s(%s)" at 0x%x>' % (self.__class__.__name__, 
                                              self.rtype,
                                              self.name,
@@ -621,9 +506,9 @@ class MainFunction(Function):
     be defined in a single code string. The code must contain a main() function
     definition.
     """
-    def __init__(self, *args, **kwds):
+    def __init__(self, *args, **kwargs):
         self._chains = {}
-        Function.__init__(self, *args, **kwds)
+        Function.__init__(self, *args, **kwargs)
     
     @property
     def signature(self):
@@ -659,306 +544,6 @@ class MainFunction(Function):
         self._chains[hook].remove(func)
 
 
-class Variable(ShaderObject):
-    """ Representation of global shader variable
-    
-    Parameters
-    ----------
-    name : str
-        the name of the variable. This string can also contain the full
-        definition of the variable, e.g. 'uniform vec2 foo'.
-    value : {float, int, tuple, GLObject}
-        If given, vtype and dtype are determined automatically. If a
-        float/int/tuple is given, the variable is a uniform. If a gloo
-        object is given that has a glsl_type property, the variable is
-        an attribute and
-    vtype : {'const', 'uniform', 'attribute', 'varying', 'inout'}
-        The type of variable.
-    dtype : str
-        The data type of the variable, e.g. 'float', 'vec4', 'mat', etc.
-    
-    """
-    def __init__(self, name, value=None, vtype=None, dtype=None):
-        super(Variable, self).__init__()
-        
-        # allow full definition in first argument
-        if ' ' in name:
-            fields = name.split(' ')
-            if len(fields) == 3:
-                vtype, dtype, name = fields
-            elif len(fields) == 4 and fields[0] == 'const':
-                vtype, dtype, name, value = fields
-            else:
-                raise ValueError('Variable specifications given by string must'
-                                 ' be of the form "vtype dtype name" or '
-                                 '"const dtype name value".')
-            
-        if not (isinstance(name, string_types) or name is None):
-            raise TypeError("Variable name must be string or None.")
-        
-        self._state_counter = 0
-        self._name = name
-        self._vtype = vtype
-        self._dtype = dtype
-        self._value = None
-        
-        # If vtype/dtype were given at init, then we will never
-        # try to set these values automatically.
-        self._type_locked = self._vtype is not None and self._dtype is not None
-            
-        if value is not None:
-            self.value = value
-        
-        if self._vtype and self._vtype not in VARIABLE_TYPES:
-            raise ValueError('Not a valid vtype: %r' % self._vtype)
-    
-    @property
-    def name(self):
-        """ The name of this variable.
-        """
-        return self._name
-    
-    @name.setter
-    def name(self, n):
-        # Settable mostly to allow automatic setting of varying names 
-        # See ShaderObject.create()
-        if self._name != n:
-            self._name = n
-            self.changed(code_changed=True)
-    
-    @property
-    def vtype(self):
-        """ The type of variable (const, uniform, attribute, varying or inout).
-        """
-        return self._vtype
-    
-    @property
-    def dtype(self):
-        """ The type of data (float, int, vec, mat, ...).
-        """
-        return self._dtype
-    
-    @property
-    def value(self):
-        """ The value associated with this variable.
-        """
-        return self._value
-    
-    @value.setter
-    def value(self, value):
-        if isinstance(value, (tuple, list)) and 1 < len(value) < 5:
-            vtype = 'uniform'
-            dtype = 'vec%d' % len(value)
-        elif isinstance(value, np.ndarray):
-            if value.ndim == 1 and (1 < len(value) < 5):
-                vtype = 'uniform'
-                dtype = 'vec%d' % len(value)
-            elif value.ndim == 2 and value.shape in ((2, 2), (3, 3), (4, 4)):
-                vtype = 'uniform'
-                dtype = 'mat%d' % value.shape[0]                
-            else:
-                raise ValueError("Cannot make uniform value for %s from array "
-                                 "of shape %s." % (self.name, value.shape))
-        elif np.isscalar(value):
-            vtype = 'uniform'
-            if isinstance(value, (float, np.floating)):
-                dtype = 'float'
-            elif isinstance(value, (int, np.integer)):
-                dtype = 'int'
-            else:
-                raise TypeError("Unknown data type %r for variable %r" % 
-                                (type(value), self))
-        elif getattr(value, 'glsl_type', None) is not None:
-            # Note: hasattr() is broken by design--swallows all exceptions!
-            vtype, dtype = value.glsl_type
-        else:
-            raise TypeError("Unknown data type %r for variable %r" % 
-                            (type(value), self))
-
-        self._value = value
-        self._state_counter += 1
-        
-        if self._type_locked:
-            if dtype != self._dtype or vtype != self._vtype:
-                raise TypeError('Variable is type "%s"; cannot assign value '
-                                '%r.' % (self.dtype, value))
-            return
-            
-        # update vtype/dtype and emit changed event if necessary
-        changed = False
-        if self._dtype != dtype:
-            self._dtype = dtype
-            changed = True
-        if self._vtype != vtype:
-            self._vtype = vtype
-            changed = True
-        if changed:
-            self.changed(code_changed=True, value_changed=True)
-    
-    @property
-    def state_id(self):
-        """Return a unique ID that changes whenever the state of the Variable
-        has changed. This allows ModularProgram to quickly determine whether
-        the value has changed since it was last used."""
-        return id(self), self._state_counter
-
-    def __repr__(self):
-        return ("<%s \"%s %s %s\" at 0x%x>" % (self.__class__.__name__,
-                                               self._vtype, self._dtype, 
-                                               self.name, id(self)))
-    
-    def expression(self, names):
-        return names[self]
-    
-    def definition(self, names):
-        if self.vtype is None:
-            raise RuntimeError("Variable has no vtype: %r" % self)
-        if self.dtype is None:
-            raise RuntimeError("Variable has no dtype: %r" % self)
-        
-        name = names[self]
-        if self.vtype == 'const':
-            return '%s %s %s = %s;' % (self.vtype, self.dtype, name, 
-                                       self.value)
-        else:
-            return '%s %s %s;' % (self.vtype, self.dtype, name)
-
-
-class Varying(Variable):
-    """ Representation of a varying
-    
-    Varyings can inherit their dtype from another Variable, allowing for
-    more flexibility in composing shaders.
-    """
-    def __init__(self, name, dtype=None):
-        self._link = None
-        Variable.__init__(self, name, vtype='varying', dtype=dtype)
-        
-    @property
-    def value(self):
-        """ The value associated with this variable.
-        """
-        return self._value
-    
-    @value.setter
-    def value(self, value):
-        if value is not None:
-            raise TypeError("Cannot assign value directly to varying.")
-    
-    @property
-    def dtype(self):
-        if self._dtype is None:
-            if self._link is None:
-                return None
-            else:
-                return self._link.dtype
-        else:
-            return self._dtype
-
-    def link(self, var):
-        """ Link this Varying to another object from which it will derive its
-        dtype. This method is used internally when assigning an attribute to
-        a varying using syntax ``aFunction[varying] = attr``.
-        """
-        assert self._dtype is not None or hasattr(var, 'dtype')
-        self._link = var
-        self.changed()
-
-
-class Expression(ShaderObject):
-    """ Base class for expressions (ShaderObjects that do not have a
-    definition nor dependencies)
-    """
-    
-    def definition(self, names):
-        # expressions are declared inline.
-        return None
-
-
-class TextExpression(Expression):
-    """ Plain GLSL text to insert inline
-    """
-    
-    def __init__(self, text):
-        super(TextExpression, self).__init__()
-        if not isinstance(text, string_types):
-            raise TypeError("Argument must be string.")
-        self._text = text
-    
-    def __repr__(self):
-        return '<TextExpression %r for at 0x%x>' % (self.text, id(self))
-    
-    def expression(self, names=None):
-        return self._text
-    
-    @property
-    def text(self):
-        return self._text
-    
-    @text.setter
-    def text(self, t):
-        self._text = t
-        self.changed()
-
-    def __eq__(self, a):
-        if isinstance(a, TextExpression):
-            return a._text == self._text
-        elif isinstance(a, string_types):
-            return a == self._text
-        else:
-            return False
-    
-    def __hash__(self):
-        return self._text.__hash__()
-
-
-class FunctionCall(Expression):
-    """ Representation of a call to a function
-    
-    Essentially this is container for a Function along with its signature. 
-    """
-    def __init__(self, function, args):
-        super(FunctionCall, self).__init__()
-        
-        if not isinstance(function, Function):
-            raise TypeError('FunctionCall needs a Function')
-        
-        sig_len = len(function.args)
-        if len(args) != sig_len:
-            raise TypeError('Function %s requires %d arguments (got %d)' %
-                            (function.name, sig_len, len(args)))
-        
-        # Ensure all expressions
-        sig = function.args
-        
-        self._function = function
-        
-        # Convert all arguments to ShaderObject, using arg name if possible.
-        self._args = [ShaderObject.create(arg, ref=sig[i][1]) 
-                      for i, arg in enumerate(args)]
-        
-        self._add_dep(function)
-        for arg in self._args:
-            self._add_dep(arg)
-    
-    def __repr__(self):
-        return '<FunctionCall of %r at 0x%x>' % (self.function.name, id(self))
-    
-    @property
-    def function(self):
-        return self._function
-    
-    @property
-    def dtype(self):
-        return self._function.rtype
-    
-    def expression(self, names):
-        str_args = [arg.expression(names) for arg in self._args]
-        args = ', '.join(str_args)
-        fname = self.function.expression(names)
-        return '%s(%s)' % (fname, args)
-
-
 class FunctionChain(Function):
     """Subclass that generates GLSL code to call Function list in order
 
@@ -1132,3 +717,39 @@ class FunctionChain(Function):
     def __repr__(self):
         fn = ",\n                ".join(map(repr, self.functions))
         return "<FunctionChain [%s] at 0x%x>" % (fn, id(self))
+
+
+class StatementList(ShaderObject):
+    """Represents a list of statements. 
+    """
+    def __init__(self):
+        self.items = []
+        ShaderObject.__init__(self)
+        
+    def append(self, item):
+        self.items.append(item)
+        self._add_dep(item)
+        self.changed(code_changed=True)
+
+    def add(self, item):
+        """Add an item to the list unless it is already present.
+        
+        If the item is an expression, then a semicolon will be appended to it
+        in the final compiled code.
+        """
+        if item in self.items:
+            return
+        self.append(item)
+        
+    def remove(self, item):
+        """Remove an item from the list.
+        """
+        self.items.remove(item)
+        self._remove_dep(item)
+        self.changed(code_changed=True)
+
+    def expression(self, obj_names):
+        code = ""
+        for item in self.items:
+            code += item.expression(obj_names) + ';\n'
+        return code
diff --git a/vispy/scene/shaders/parsing.py b/vispy/visuals/shaders/parsing.py
similarity index 94%
rename from vispy/scene/shaders/parsing.py
rename to vispy/visuals/shaders/parsing.py
index 00ba32b..2450052 100644
--- a/vispy/scene/shaders/parsing.py
+++ b/vispy/visuals/shaders/parsing.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
@@ -7,7 +7,8 @@ from __future__ import division
 import re
 
 # regular expressions for parsing GLSL
-re_type = r'(?:void|int|float|vec2|vec3|vec4|mat2|mat3|mat4)'
+re_type = r'(?:void|int|float|vec2|vec3|vec4|mat2|mat3|mat4|\
+            sampler1D|sampler2D|sampler3D)'
 re_identifier = r'(?:[a-zA-Z_][\w_]*)'
 
 # variable qualifiers
@@ -15,8 +16,8 @@ re_qualifier = r'(const|uniform|attribute|varying)'
 
 # template variables like
 #     $func_name
-re_template_var = (r"(?:(?:\$" + re_identifier + ")|(?:\$\{"
-                   + re_identifier + "\}))")
+re_template_var = (r"(?:(?:\$" + re_identifier + ")|(?:\$\{" +
+                   re_identifier + "\}))")
 
 # function names may be either identifier or template var
 re_func_name = r"(" + re_identifier + "|" + re_template_var + ")"
diff --git a/vispy/visuals/shaders/program.py b/vispy/visuals/shaders/program.py
new file mode 100644
index 0000000..3b53533
--- /dev/null
+++ b/vispy/visuals/shaders/program.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division
+
+from ...gloo import Program
+from ...gloo.preprocessor import preprocess
+from ...util import logger
+from ...util.event import EventEmitter
+from .function import MainFunction
+from .variable import Variable
+from .compiler import Compiler
+
+
+class ModularProgram(Program):
+    """
+    Shader program using Function instances as basis for its shaders.
+
+    Automatically rebuilds program when functions have changed and uploads
+    program variables.
+    """
+    def __init__(self, vcode=None, fcode=None):
+        Program.__init__(self)
+
+        self.changed = EventEmitter(source=self, type='program_change')
+
+        # Cache state of Variables so we know which ones require update
+        self._variable_state = {}
+
+        self.vert = vcode
+        self.frag = fcode
+
+    @property
+    def vert(self):
+        return self._vert
+
+    @vert.setter
+    def vert(self, vcode):
+        if hasattr(self, '_vert') and self._vert is not None:
+            self._vert._dependents.pop(self)
+
+        self._vert = vcode
+        if self._vert is None:
+            return
+
+        vcode = preprocess(vcode)
+        self._vert = MainFunction(vcode)
+        self._vert._dependents[self] = None
+
+        self._need_build = True
+        self.changed(code_changed=True, value_changed=False)
+
+    @property
+    def frag(self):
+        return self._frag
+
+    @frag.setter
+    def frag(self, fcode):
+        if hasattr(self, '_frag') and self._frag is not None:
+            self._frag._dependents.pop(self)
+
+        self._frag = fcode
+        if self._frag is None:
+            return
+
+        fcode = preprocess(fcode)
+        self._frag = MainFunction(fcode)
+        self._frag._dependents[self] = None
+
+        self._need_build = True
+        self.changed(code_changed=True, value_changed=False)
+
+    def prepare(self):
+        """ Prepare the Program so we can set attributes and uniforms.
+        """
+        pass
+        # todo: remove!
+
+    def _dep_changed(self, dep, code_changed=False, value_changed=False):
+        logger.debug("ModularProgram source changed: %s", self)
+        if code_changed:
+            self._need_build = True
+        self.changed(code_changed=code_changed, 
+                     value_changed=value_changed)
+    
+    def draw(self, *args, **kwargs):
+        self.build_if_needed()
+        Program.draw(self, *args, **kwargs)
+
+    def build_if_needed(self):
+        """ Reset shader source if necesssary.
+        """
+        if self._need_build:
+            self._build()
+            self._need_build = False
+        self.update_variables()
+
+    def _build(self):
+        logger.debug("Rebuild ModularProgram: %s", self)
+        self.compiler = Compiler(vert=self.vert, frag=self.frag)
+        code = self.compiler.compile()
+        self.set_shaders(code['vert'], code['frag'])
+        logger.debug('==== Vertex Shader ====\n\n%s\n', code['vert'])
+        logger.debug('==== Fragment shader ====\n\n%s\n', code['frag'])
+        # Note: No need to reset _variable_state, gloo.Program resends
+        # attribute/uniform data on setting shaders
+
+    def update_variables(self):
+        # Clear any variables that we may have set another time.
+        # Otherwise we get lots of warnings.
+        self._pending_variables = {}
+        # set all variables
+        settable_vars = 'attribute', 'uniform'
+        logger.debug("Apply variables:")
+        deps = self.vert.dependencies() + self.frag.dependencies()
+        for dep in deps:
+            if not isinstance(dep, Variable) or dep.vtype not in settable_vars:
+                continue
+            name = self.compiler[dep]
+            logger.debug("    %s = %s", name, dep.value)
+            state_id = dep.state_id
+            if self._variable_state.get(name, None) != state_id:
+                self[name] = dep.value
+                self._variable_state[name] = state_id
diff --git a/vispy/visuals/shaders/shader_object.py b/vispy/visuals/shaders/shader_object.py
new file mode 100644
index 0000000..5c6272e
--- /dev/null
+++ b/vispy/visuals/shaders/shader_object.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+from weakref import WeakKeyDictionary
+
+from ...util import logger
+from ...ext.ordereddict import OrderedDict
+from ...ext.six import string_types
+from .compiler import Compiler
+
+
+class ShaderObject(object):
+    """ Base class for all objects that may be included in a GLSL program
+    (Functions, Variables, Expressions).
+    
+    Shader objects have a *definition* that defines the object in GLSL, an 
+    *expression* that is used to reference the object, and a set of 
+    *dependencies* that must be declared before the object is used.
+    
+    Dependencies are tracked hierarchically such that changes to any object
+    will be propagated up the dependency hierarchy to trigger a recompile.
+    """
+
+    @classmethod
+    def create(self, obj, ref=None):
+        """ Convert *obj* to a new ShaderObject. If the output is a Variable
+        with no name, then set its name using *ref*. 
+        """
+        if isinstance(ref, Variable):
+            ref = ref.name
+        elif isinstance(ref, string_types) and ref.startswith('gl_'):
+            # gl_ names not allowed for variables
+            ref = ref[3:].lower()
+        
+        # Allow any type of object to be converted to ShaderObject if it
+        # provides a magic method:
+        if hasattr(obj, '_shader_object'):
+            obj = obj._shader_object()
+        
+        if isinstance(obj, ShaderObject):
+            if isinstance(obj, Variable) and obj.name is None:
+                obj.name = ref
+        elif isinstance(obj, string_types):
+            obj = TextExpression(obj)
+        else:
+            obj = Variable(ref, obj)
+            # Try prepending the name to indicate attribute, uniform, varying
+            if obj.vtype and obj.vtype[0] in 'auv':
+                obj.name = obj.vtype[0] + '_' + obj.name 
+        
+        return obj
+    
+    def __init__(self):
+        # objects that must be declared before this object's definition.
+        # {obj: refcount}
+        self._deps = OrderedDict()  # OrderedDict for consistent code output
+        
+        # Objects that depend on this one will be informed of changes.
+        self._dependents = WeakKeyDictionary()
+    
+    @property
+    def name(self):
+        """ The name of this shader object.
+        """
+        return None
+        
+    def definition(self, obj_names):
+        """ Return the GLSL definition for this object. Use *obj_names* to
+        determine the names of dependencies.
+        """
+        return None
+    
+    def expression(self, obj_names):
+        """ Return the GLSL expression used to reference this object inline.
+        """
+        return obj_names[self]
+    
+    def dependencies(self, sort=False):
+        """ Return all dependencies required to use this object. The last item 
+        in the list is *self*.
+        """
+        alldeps = []
+        if sort:
+            def key(obj):
+                # sort deps such that we get functions, variables, self.
+                if not isinstance(obj, Variable):
+                    return (0, 0)
+                else:
+                    return (1, obj.vtype)
+            
+            deps = sorted(self._deps, key=key)
+        else:
+            deps = self._deps
+        
+        for dep in deps:
+            alldeps.extend(dep.dependencies(sort=sort))
+        alldeps.append(self)
+        return alldeps
+
+    def static_names(self):
+        """ Return a list of names that are declared in this object's 
+        definition (not including the name of the object itself).
+        
+        These names will be reserved by the compiler when automatically 
+        determining object names.
+        """
+        return []
+    
+    def _add_dep(self, dep):
+        """ Increment the reference count for *dep*. If this is a new 
+        dependency, then connect to its *changed* event.
+        """
+        if dep in self._deps:
+            self._deps[dep] += 1
+        else:
+            self._deps[dep] = 1
+            dep._dependents[self] = None
+
+    def _remove_dep(self, dep):
+        """ Decrement the reference count for *dep*. If the reference count 
+        reaches 0, then the dependency is removed and its *changed* event is
+        disconnected.
+        """
+        refcount = self._deps[dep]
+        if refcount == 1:
+            self._deps.pop(dep)
+            dep._dependents.pop(self)
+        else:
+            self._deps[dep] -= 1
+
+    def _dep_changed(self, dep, code_changed=False, value_changed=False):
+        """ Called when a dependency's expression has changed.
+        """
+        logger.debug("ShaderObject changed [code=%s, value=%s]", code_changed,
+                     value_changed)
+        self.changed(code_changed, value_changed)
+            
+    def changed(self, code_changed=False, value_changed=False):
+        """Inform dependents that this shaderobject has changed.
+        """
+        for d in self._dependents:
+            d._dep_changed(self, code_changed=code_changed,
+                           value_changed=value_changed)
+    
+    def compile(self):
+        """ Return a compilation of this object and its dependencies. 
+        
+        Note: this is mainly for debugging purposes; the names in this code
+        are not guaranteed to match names in any other compilations. Use
+        Compiler directly to ensure consistent naming across multiple objects. 
+        """
+        compiler = Compiler(obj=self)
+        return compiler.compile()['obj']
+    
+    def __repr__(self):
+        if self.name is not None:
+            return '<%s "%s" at 0x%x>' % (self.__class__.__name__, 
+                                          self.name, id(self))
+        else:
+            return '<%s at 0x%x>' % (self.__class__.__name__, id(self))
+
+
+from .variable import Variable  # noqa
+from .expression import TextExpression  # noqa
diff --git a/vispy/scene/shaders/tests/__init__.py b/vispy/visuals/shaders/tests/__init__.py
similarity index 100%
rename from vispy/scene/shaders/tests/__init__.py
rename to vispy/visuals/shaders/tests/__init__.py
diff --git a/vispy/scene/shaders/tests/test_function.py b/vispy/visuals/shaders/tests/test_function.py
similarity index 88%
rename from vispy/scene/shaders/tests/test_function.py
rename to vispy/visuals/shaders/tests/test_function.py
index fd87aab..913696f 100644
--- a/vispy/scene/shaders/tests/test_function.py
+++ b/vispy/visuals/shaders/tests/test_function.py
@@ -1,11 +1,15 @@
-from vispy.scene.shaders.function import (Function, Variable, Varying,
-                                          MainFunction, FunctionChain)
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+from vispy.visuals.shaders import (Function, MainFunction, Variable, Varying,
+                                   FunctionChain, StatementList)
+
 
 # Users normally don't need these, but I want to test them
-from vispy.scene.shaders.function import FunctionCall, TextExpression
+from vispy.visuals.shaders.expression import FunctionCall, TextExpression
 
-from nose.tools import assert_raises, assert_equal, assert_not_equal  # noqa
-from vispy.testing import assert_in, assert_not_in, assert_is  # noqa
+from vispy.testing import (assert_in, assert_not_in, assert_is,
+                           run_tests_if_main, assert_raises, assert_equal)
 
 
 ## Define some snippets
@@ -275,6 +279,13 @@ def test_function_basics():
     assert_in('\ntransform_scale(XX);\n', text)
     assert_in('\ntransform_scale(YY);\n', text)
     
+    # test pre/post assignments
+    fun = Function('void main() {some stuff;}')
+    fun['pre'] = '__pre__'
+    fun['post'] = '__post__'
+    text = fun.compile()
+    assert text == 'void main() {\n    __pre__\nsome stuff;\n    __post__\n}\n'
+    
     # Test variable expressions
     fun = Function('void main(){$foo; $bar;}')
     fun['foo'] = Variable('uniform float bla')
@@ -303,8 +314,10 @@ def test_function_basics():
 def test_function_changed():
     ch = []
     
-    def on_change(event):
-        ch.append(event.source)
+    class C(object):
+        def _dep_changed(self, dep, **kwargs):
+            ch.append(dep)
+    ch_obj = C()
         
     def assert_changed(*objs):
         assert set(ch) == set(objs)
@@ -312,7 +325,7 @@ def test_function_changed():
             ch.pop()
         
     fun1 = Function('void main(){$var1; $var2;}')
-    fun1.changed.connect(on_change)
+    fun1._dependents[ch_obj] = None
     fun1['var1'] = 'x'
     fun1['var2'] = 'y'
     assert_changed(fun1)
@@ -326,7 +339,7 @@ def test_function_changed():
     
     fun1['var1'] = 0.5
     var1 = fun1['var1']
-    var1.changed.connect(on_change)
+    var1._dependents[ch_obj] = None
     assert_changed(fun1)
     
     var1.name = 'xxx'
@@ -343,7 +356,7 @@ def test_function_changed():
     # test variable disconnect
     fun1['var1'] = Variable('var1', 7)
     var2 = fun1['var1']
-    var2.changed.connect(on_change)
+    var2._dependents[ch_obj] = None
     #assert_changed(fun1)
     # var2 is now connected
     var2.value = (1, 2, 3, 4)
@@ -360,9 +373,9 @@ def test_function_changed():
     fun1['var2'] = exp1
     assert_changed(fun1)
     
-    fun2.changed.connect(on_change)
-    fun3.changed.connect(on_change)
-    exp1.changed.connect(on_change)
+    fun2._dependents[ch_obj] = None
+    fun3._dependents[ch_obj] = None
+    exp1._dependents[ch_obj] = None
     
     fun2['var1'] = 'x'
     assert_changed(fun1, fun2, exp1)
@@ -414,6 +427,23 @@ def test_FunctionChain():
     assert_in(f5, ch.dependencies())
 
 
+def test_StatementList():
+    func = Function("void func() {}")
+    main = Function("void main() {}")
+    main['pre'] = StatementList()
+    expr = func()
+    main['pre'].append(expr)
+    assert main['pre'].items == [expr]
+    main['pre'].add(expr)
+    assert main['pre'].items == [expr]
+    
+    code = main.compile()
+    assert " func();" in code
+    
+    main['pre'].remove(expr)
+    assert main['pre'].items == []
+
+
 def test_MainFunction():
     code = """
     const float pi = 3.0;  // close enough.
@@ -448,3 +478,6 @@ if __name__ == '__main__':
     # Uncomment to run example
     print('='*80)
     test_example1()
+
+
+run_tests_if_main()
diff --git a/vispy/scene/shaders/tests/test_parsing.py b/vispy/visuals/shaders/tests/test_parsing.py
similarity index 75%
rename from vispy/scene/shaders/tests/test_parsing.py
rename to vispy/visuals/shaders/tests/test_parsing.py
index 81731bb..12b0161 100644
--- a/vispy/scene/shaders/tests/test_parsing.py
+++ b/vispy/visuals/shaders/tests/test_parsing.py
@@ -1,12 +1,17 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import re
-from vispy.scene.shaders.parsing import re_identifier, find_program_variables
+
+from vispy.visuals.shaders.parsing import re_identifier, find_program_variables
+from vispy.testing import run_tests_if_main
 
 
 def test_identifier():
-    assert(re.match('('+re_identifier+')', 'Ax2_d3__7').groups()[0]
-           == 'Ax2_d3__7')
-    assert(re.match('('+re_identifier+')', '_Ax2_d3__7').groups()[0]
-           == '_Ax2_d3__7')
+    assert(re.match('('+re_identifier+')', 'Ax2_d3__7').groups()[0] ==
+           'Ax2_d3__7')
+    assert(re.match('('+re_identifier+')', '_Ax2_d3__7').groups()[0] ==
+           '_Ax2_d3__7')
     assert(re.match(re_identifier, '7Ax2_d3__7') is None)
     assert(re.match('('+re_identifier+')', 'x,y').groups()[0] == 'x')
     assert(re.match('('+re_identifier+')', 'x y').groups()[0] == 'x')
@@ -47,3 +52,6 @@ def test_find_variables():
         assert expect[k] == vars.pop(k)
         
     assert len(vars) == 0
+
+
+run_tests_if_main()
diff --git a/vispy/visuals/shaders/variable.py b/vispy/visuals/shaders/variable.py
new file mode 100644
index 0000000..8200248
--- /dev/null
+++ b/vispy/visuals/shaders/variable.py
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+import numpy as np
+from ...ext.six import string_types
+from .shader_object import ShaderObject
+
+VARIABLE_TYPES = ('const', 'uniform', 'attribute', 'varying', 'inout')
+
+
+class Variable(ShaderObject):
+    """ Representation of global shader variable
+    
+    Parameters
+    ----------
+    name : str
+        the name of the variable. This string can also contain the full
+        definition of the variable, e.g. 'uniform vec2 foo'.
+    value : {float, int, tuple, GLObject}
+        If given, vtype and dtype are determined automatically. If a
+        float/int/tuple is given, the variable is a uniform. If a gloo
+        object is given that has a glsl_type property, the variable is
+        an attribute and
+    vtype : {'const', 'uniform', 'attribute', 'varying', 'inout'}
+        The type of variable.
+    dtype : str
+        The data type of the variable, e.g. 'float', 'vec4', 'mat', etc.
+    
+    """
+    def __init__(self, name, value=None, vtype=None, dtype=None):
+        super(Variable, self).__init__()
+        
+        # allow full definition in first argument
+        if ' ' in name:
+            fields = name.split(' ')
+            if len(fields) == 3:
+                vtype, dtype, name = fields
+            elif len(fields) == 4 and fields[0] == 'const':
+                vtype, dtype, name, value = fields
+            else:
+                raise ValueError('Variable specifications given by string must'
+                                 ' be of the form "vtype dtype name" or '
+                                 '"const dtype name value".')
+            
+        if not (isinstance(name, string_types) or name is None):
+            raise TypeError("Variable name must be string or None.")
+        
+        self._state_counter = 0
+        self._name = name
+        self._vtype = vtype
+        self._dtype = dtype
+        self._value = None
+        
+        # If vtype/dtype were given at init, then we will never
+        # try to set these values automatically.
+        self._type_locked = self._vtype is not None and self._dtype is not None
+            
+        if value is not None:
+            self.value = value
+        
+        if self._vtype and self._vtype not in VARIABLE_TYPES:
+            raise ValueError('Not a valid vtype: %r' % self._vtype)
+    
+    @property
+    def name(self):
+        """ The name of this variable.
+        """
+        return self._name
+    
+    @name.setter
+    def name(self, n):
+        # Settable mostly to allow automatic setting of varying names 
+        # See ShaderObject.create()
+        if self._name != n:
+            self._name = n
+            self.changed(code_changed=True)
+    
+    @property
+    def vtype(self):
+        """ The type of variable (const, uniform, attribute, varying or inout).
+        """
+        return self._vtype
+    
+    @property
+    def dtype(self):
+        """ The type of data (float, int, vec, mat, ...).
+        """
+        return self._dtype
+    
+    @property
+    def value(self):
+        """ The value associated with this variable.
+        """
+        return self._value
+    
+    @value.setter
+    def value(self, value):
+        if isinstance(value, (tuple, list)) and 1 < len(value) < 5:
+            vtype = 'uniform'
+            dtype = 'vec%d' % len(value)
+        elif isinstance(value, np.ndarray):
+            if value.ndim == 1 and (1 < len(value) < 5):
+                vtype = 'uniform'
+                dtype = 'vec%d' % len(value)
+            elif value.ndim == 2 and value.shape in ((2, 2), (3, 3), (4, 4)):
+                vtype = 'uniform'
+                dtype = 'mat%d' % value.shape[0]                
+            else:
+                raise ValueError("Cannot make uniform value for %s from array "
+                                 "of shape %s." % (self.name, value.shape))
+        elif np.isscalar(value):
+            vtype = 'uniform'
+            if isinstance(value, (float, np.floating)):
+                dtype = 'float'
+            elif isinstance(value, (int, np.integer)):
+                dtype = 'int'
+            else:
+                raise TypeError("Unknown data type %r for variable %r" % 
+                                (type(value), self))
+        elif getattr(value, 'glsl_type', None) is not None:
+            # Note: hasattr() is broken by design--swallows all exceptions!
+            vtype, dtype = value.glsl_type
+        else:
+            raise TypeError("Unknown data type %r for variable %r" % 
+                            (type(value), self))
+
+        self._value = value
+        self._state_counter += 1
+        
+        if self._type_locked:
+            if dtype != self._dtype or vtype != self._vtype:
+                raise TypeError('Variable is type "%s"; cannot assign value '
+                                '%r.' % (self.dtype, value))
+            return
+            
+        # update vtype/dtype and emit changed event if necessary
+        changed = False
+        if self._dtype != dtype:
+            self._dtype = dtype
+            changed = True
+        if self._vtype != vtype:
+            self._vtype = vtype
+            changed = True
+        if changed:
+            self.changed(code_changed=True, value_changed=True)
+    
+    @property
+    def state_id(self):
+        """Return a unique ID that changes whenever the state of the Variable
+        has changed. This allows ModularProgram to quickly determine whether
+        the value has changed since it was last used."""
+        return id(self), self._state_counter
+
+    def __repr__(self):
+        return ("<%s \"%s %s %s\" at 0x%x>" % (self.__class__.__name__,
+                                               self._vtype, self._dtype, 
+                                               self.name, id(self)))
+    
+    def expression(self, names):
+        return names[self]
+    
+    def definition(self, names):
+        if self.vtype is None:
+            raise RuntimeError("Variable has no vtype: %r" % self)
+        if self.dtype is None:
+            raise RuntimeError("Variable has no dtype: %r" % self)
+        
+        name = names[self]
+        if self.vtype == 'const':
+            return '%s %s %s = %s;' % (self.vtype, self.dtype, name, 
+                                       self.value)
+        else:
+            return '%s %s %s;' % (self.vtype, self.dtype, name)
+
+
+class Varying(Variable):
+    """ Representation of a varying
+    
+    Varyings can inherit their dtype from another Variable, allowing for
+    more flexibility in composing shaders.
+    """
+    def __init__(self, name, dtype=None):
+        self._link = None
+        Variable.__init__(self, name, vtype='varying', dtype=dtype)
+        
+    @property
+    def value(self):
+        """ The value associated with this variable.
+        """
+        return self._value
+    
+    @value.setter
+    def value(self, value):
+        if value is not None:
+            raise TypeError("Cannot assign value directly to varying.")
+    
+    @property
+    def dtype(self):
+        if self._dtype is None:
+            if self._link is None:
+                return None
+            else:
+                return self._link.dtype
+        else:
+            return self._dtype
+
+    def link(self, var):
+        """ Link this Varying to another object from which it will derive its
+        dtype. This method is used internally when assigning an attribute to
+        a varying using syntax ``Function[varying] = attr``.
+        """
+        assert self._dtype is not None or hasattr(var, 'dtype')
+        self._link = var
+        self.changed()
diff --git a/vispy/visuals/spectrogram.py b/vispy/visuals/spectrogram.py
new file mode 100644
index 0000000..1bb889e
--- /dev/null
+++ b/vispy/visuals/spectrogram.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+import numpy as np
+
+from .image import ImageVisual
+from ..util.fourier import stft, fft_freqs
+from ..ext.six import string_types
+
+
+class SpectrogramVisual(ImageVisual):
+    """Calculate and show a spectrogram
+
+    Parameters
+    ----------
+    x : array-like
+        1D signal to operate on. ``If len(x) < n_fft``, x will be
+        zero-padded to length ``n_fft``.
+    n_fft : int
+        Number of FFT points. Much faster for powers of two.
+    step : int | None
+        Step size between calculations. If None, ``n_fft // 2``
+        will be used.
+    fs : float
+        The sample rate of the data.
+    window : str | None
+        Window function to use. Can be ``'hann'`` for Hann window, or None
+        for no windowing.
+    color_scale : {'linear', 'log'}
+        Scale to apply to the result of the STFT.
+        ``'log'`` will use ``10 * log10(power)``.
+    cmap : str
+        Colormap name.
+    clim : str | tuple
+        Colormap limits. Should be ``'auto'`` or a two-element tuple of
+        min and max values.
+    """
+    def __init__(self, x, n_fft=256, step=None, fs=1., window='hann',
+                 color_scale='log', cmap='cubehelix', clim='auto'):
+        self._n_fft = int(n_fft)
+        self._fs = float(fs)
+        if not isinstance(color_scale, string_types) or \
+                color_scale not in ('log', 'linear'):
+            raise ValueError('color_scale must be "linear" or "log"')
+        data = stft(x, self._n_fft, step, self._fs, window)
+        data = np.abs(data)
+        data = 20 * np.log10(data) if color_scale == 'log' else data
+        super(SpectrogramVisual, self).__init__(data, clim=clim, cmap=cmap)
+
+    @property
+    def freqs(self):
+        """The spectrogram frequencies"""
+        return fft_freqs(self._n_fft, self._fs)
diff --git a/vispy/scene/visuals/surface_plot.py b/vispy/visuals/surface_plot.py
similarity index 93%
rename from vispy/scene/visuals/surface_plot.py
rename to vispy/visuals/surface_plot.py
index 40ccc63..cd1c621 100644
--- a/vispy/scene/visuals/surface_plot.py
+++ b/vispy/visuals/surface_plot.py
@@ -1,16 +1,16 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
 
 import numpy as np
 
-from .mesh import Mesh
-from ...geometry import MeshData
+from .mesh import MeshVisual
+from ..geometry import MeshData
 
 
-class SurfacePlot(Mesh):
+class SurfacePlotVisual(MeshVisual):
     """Displays a surface plot on a regular x,y grid
 
     Parameters
@@ -36,23 +36,23 @@ class SurfacePlot(Mesh):
     For faster performance, initialize with compute_normals=False and use
     per-vertex colors or a material that does not require normals.
     """
-    def __init__(self, x=None, y=None, z=None, colors=None, **kwds):
+    def __init__(self, x=None, y=None, z=None, colors=None, **kwargs):
         # The x, y, z, and colors arguments are passed to set_data().
-        # All other keyword arguments are passed to Mesh.__init__().
+        # All other keyword arguments are passed to MeshVisual.__init__().
         self._x = None
         self._y = None
         self._z = None
         self.__color = None
         self.__vertices = None
         self.__meshdata = MeshData()
-        Mesh.__init__(self, **kwds)
+        MeshVisual.__init__(self, **kwargs)
         self.set_data(x, y, z, colors)
 
     def set_data(self, x=None, y=None, z=None, colors=None):
         """Update the data in this surface plot.
 
-        Parameters:
-        -----------
+        Parameters
+        ----------
         x : ndarray | None
             1D array of values specifying the x positions of vertices in the
             grid. If None, values will be assumed to be integers.
@@ -131,7 +131,7 @@ class SurfacePlot(Mesh):
             self.__meshdata.set_vertices(
                 self.__vertices.reshape(self.__vertices.shape[0] *
                                         self.__vertices.shape[1], 3))
-            Mesh.set_data(self, meshdata=self.__meshdata)
+            MeshVisual.set_data(self, meshdata=self.__meshdata)
 
     def generate_faces(self):
         cols = self._z.shape[1]-1
diff --git a/vispy/visuals/tests/test_collections.py b/vispy/visuals/tests/test_collections.py
new file mode 100644
index 0000000..c7a5bfe
--- /dev/null
+++ b/vispy/visuals/tests/test_collections.py
@@ -0,0 +1,16 @@
+# *Very* basic collections tests
+
+from vispy.visuals.collections import (PathCollection, PointCollection,
+                                       PolygonCollection, SegmentCollection,
+                                       TriangleCollection)
+from vispy.testing import requires_application, TestingCanvas
+
+
+ at requires_application()
+def test_init():
+    """Test collection initialization
+    """
+    with TestingCanvas():
+        for coll in (PathCollection, PointCollection, PolygonCollection,
+                     SegmentCollection, TriangleCollection):
+            coll()
diff --git a/vispy/scene/visuals/tests/test_ellipse.py b/vispy/visuals/tests/test_ellipse.py
similarity index 65%
rename from vispy/scene/visuals/tests/test_ellipse.py
rename to vispy/visuals/tests/test_ellipse.py
index 9f3269f..d30cdd6 100644
--- a/vispy/scene/visuals/tests/test_ellipse.py
+++ b/vispy/visuals/tests/test_ellipse.py
@@ -1,14 +1,16 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
 Tests for EllipseVisual
 All images are of size (100,100) to keep a small file size
 """
 
-from vispy import gloo
 from vispy.scene import visuals, transforms
-from vispy.testing import (requires_application, assert_image_equal,
-                           TestingCanvas)
+from vispy.testing import (requires_application, TestingCanvas, 
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
 
 
 @requires_application()
@@ -18,20 +20,21 @@ def test_circle_draw():
         ellipse = visuals.Ellipse(pos=(75, 35, 0), radius=20,
                                   color=(1, 0, 0, 1))
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/circle1.png')
+        assert_image_approved("screenshot", 'visuals/circle1.png')
 
-        gloo.clear()
         ellipse = visuals.Ellipse(pos=(75, 35, 0), radius=20,
                                   color=(1, 0, 0, 1),
                                   border_color=(0, 1, 1, 1))
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/circle2.png')
+        assert_image_approved("screenshot", 'visuals/circle2.png')
 
-        gloo.clear()
         ellipse = visuals.Ellipse(pos=(75, 35, 0), radius=20,
                                   border_color=(0, 1, 1, 1))
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/circle3.png')
+        # low corr here because borders have some variability
+        # esp. w/HiDPI
+        assert_image_approved("screenshot", 'visuals/circle3.png', 
+                              min_corr=0.7)
 
 
 @requires_application()
@@ -43,24 +46,23 @@ def test_ellipse_draw():
         ellipse.transform = transforms.STTransform(scale=(2.0, 3.0),
                                                    translate=(50, 50))
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/ellipse1.png')
+        assert_image_approved("screenshot", 'visuals/ellipse1.png')
 
-        gloo.clear()
         ellipse = visuals.Ellipse(pos=(0., 0.), radius=(20, 15),
                                   color=(0, 0, 1, 1),
                                   border_color=(1, 0, 0, 1))
         ellipse.transform = transforms.STTransform(scale=(2.0, 3.0),
                                                    translate=(50, 50))
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/ellipse2.png')
+        assert_image_approved("screenshot", 'visuals/ellipse2.png')
 
-        gloo.clear()
         ellipse = visuals.Ellipse(pos=(0., 0.), radius=(20, 15),
                                   border_color=(1, 0, 0, 1))
         ellipse.transform = transforms.STTransform(scale=(2.0, 3.0),
                                                    translate=(50, 50))
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/ellipse3.png')
+        assert_image_approved("screenshot", 'visuals/ellipse3.png', 
+                              min_corr=0.7)
 
 
 @requires_application()
@@ -71,14 +73,14 @@ def test_arc_draw1():
                                   start_angle=150., span_angle=120.,
                                   color=(0, 0, 1, 1))
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/arc1.png')
+        assert_image_approved("screenshot", 'visuals/arc1.png')
 
-        gloo.clear()
         ellipse = visuals.Ellipse(pos=(50., 50.), radius=(20, 15),
-                                  start_angle=90., span_angle=120.,
+                                  start_angle=150., span_angle=120.,
                                   border_color=(1, 0, 0, 1))
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/arc2.png')
+        assert_image_approved("screenshot", 'visuals/arc2.png', 
+                              min_corr=0.6)
 
 
 @requires_application()
@@ -89,37 +91,33 @@ def test_reactive_draw():
                                   color='yellow')
         c.draw_visual(ellipse)
 
-        gloo.clear()
         ellipse.pos = [70, 40, 0.]
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/reactive_ellipse1.png')
+        assert_image_approved("screenshot", 'visuals/reactive_ellipse1.png')
 
-        gloo.clear()
         ellipse.radius = 25
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/reactive_ellipse2.png')
+        assert_image_approved("screenshot", 'visuals/reactive_ellipse2.png')
 
-        gloo.clear()
         ellipse.color = 'red'
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/reactive_ellipse3.png')
+        assert_image_approved("screenshot", 'visuals/reactive_ellipse3.png')
 
-        gloo.clear()
         ellipse.border_color = 'yellow'
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/reactive_ellipse4.png')
+        assert_image_approved("screenshot", 'visuals/reactive_ellipse4.png')
 
-        gloo.clear()
         ellipse.start_angle = 140.
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/reactive_ellipse5.png')
+        assert_image_approved("screenshot", 'visuals/reactive_ellipse5.png')
 
-        gloo.clear()
         ellipse.span_angle = 100.
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/reactive_ellipse6.png')
+        assert_image_approved("screenshot", 'visuals/reactive_ellipse6.png')
 
-        gloo.clear()
         ellipse.num_segments = 10.
         c.draw_visual(ellipse)
-        assert_image_equal("screenshot", 'visuals/reactive_ellipse7.png')
+        assert_image_approved("screenshot", 'visuals/reactive_ellipse7.png')
+
+
+run_tests_if_main()
diff --git a/vispy/visuals/tests/test_histogram.py b/vispy/visuals/tests/test_histogram.py
new file mode 100644
index 0000000..251c801
--- /dev/null
+++ b/vispy/visuals/tests/test_histogram.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+import numpy as np
+
+from vispy.visuals.transforms import STTransform
+from vispy.scene.visuals import Histogram
+from vispy.testing import (requires_application, TestingCanvas,
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
+
+
+ at requires_application()
+def test_histogram():
+    """Test histogram visual"""
+    size = (200, 100)
+    with TestingCanvas(size=size, bgcolor='w') as c:
+        np.random.seed(2397)
+        data = np.random.normal(size=100)
+        hist = Histogram(data, bins=20, color='k')
+        hist.transform = STTransform((size[0] // 10, -size[1] // 20, 1),
+                                     (100, size[1]))
+        c.draw_visual(hist)
+        assert_image_approved("screenshot", "visuals/histogram.png")
+
+
+run_tests_if_main()
diff --git a/vispy/visuals/tests/test_image.py b/vispy/visuals/tests/test_image.py
new file mode 100644
index 0000000..4723b0b
--- /dev/null
+++ b/vispy/visuals/tests/test_image.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+import numpy as np
+
+from vispy.scene.visuals import Image
+from vispy.testing import (requires_application, TestingCanvas,
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
+
+
+ at requires_application()
+def test_image():
+    """Test image visual"""
+    size = (100, 50)
+    with TestingCanvas(size=size, bgcolor='w') as c:
+        for three_d in (True, False):
+            shape = (size[1]-10, size[0]-10) + ((3,) if three_d else ())
+            np.random.seed(379823)
+            data = np.random.rand(*shape)
+            image = Image(data, cmap='grays', clim=[0, 1])
+            c.draw_visual(image)
+            assert_image_approved("screenshot", "visuals/image%s.png" %
+                                  ("_rgb" if three_d else "_mono"))
+
+
+run_tests_if_main()
diff --git a/vispy/visuals/tests/test_markers.py b/vispy/visuals/tests/test_markers.py
new file mode 100644
index 0000000..afb0121
--- /dev/null
+++ b/vispy/visuals/tests/test_markers.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+import numpy as np
+from vispy.scene.visuals import Markers
+from vispy.testing import (requires_application, TestingCanvas,
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
+
+
+ at requires_application()
+def test_markers():
+    """Test basic marker / point-sprite support"""
+    # this is probably too basic, but it at least ensures that point sprites
+    # work for people
+    np.random.seed(57983)
+    data = np.random.normal(size=(30, 2), loc=50, scale=10)
+    
+    with TestingCanvas() as c:
+        marker = Markers()
+        marker.set_data(data)
+        c.draw_visual(marker)
+        assert_image_approved("screenshot", "visuals/markers.png")
+
+    # Test good correlation at high-dpi
+    with TestingCanvas(px_scale=2) as c:
+        marker = Markers()
+        marker.set_data(data)
+        c.draw_visual(marker)
+        assert_image_approved("screenshot", "visuals/markers.png")
+
+
+run_tests_if_main()
diff --git a/vispy/scene/visuals/tests/test_polygon.py b/vispy/visuals/tests/test_polygon.py
similarity index 74%
rename from vispy/scene/visuals/tests/test_polygon.py
rename to vispy/visuals/tests/test_polygon.py
index d932b93..391703e 100644
--- a/vispy/scene/visuals/tests/test_polygon.py
+++ b/vispy/visuals/tests/test_polygon.py
@@ -7,10 +7,10 @@ All images are of size (100,100) to keep a small file size
 
 import numpy as np
 
-from vispy import gloo
 from vispy.scene import visuals, transforms
-from vispy.testing import (requires_application, assert_image_equal,
-                           requires_scipy, TestingCanvas)
+from vispy.testing import (requires_application, requires_scipy, TestingCanvas,
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
 
 
 @requires_application()
@@ -26,22 +26,21 @@ def test_square_draw():
         polygon.transform = transforms.STTransform(scale=(50, 50),
                                                    translate=(50, 50))
         c.draw_visual(polygon)
-        assert_image_equal("screenshot", 'visuals/square1.png')
+        assert_image_approved("screenshot", 'visuals/square1.png')
 
-        gloo.clear()
         polygon = visuals.Polygon(pos=pos, color=(1, 0, 0, 1),
                                   border_color=(1, 1, 1, 1))
         polygon.transform = transforms.STTransform(scale=(50, 50),
                                                    translate=(50, 50))
         c.draw_visual(polygon)
-        assert_image_equal("screenshot", 'visuals/square2.png')
+        assert_image_approved("screenshot", 'visuals/square2.png')
 
-        gloo.clear()
         polygon = visuals.Polygon(pos=pos, border_color=(1, 1, 1, 1))
         polygon.transform = transforms.STTransform(scale=(50, 50),
                                                    translate=(50, 50))
         c.draw_visual(polygon)
-        assert_image_equal("screenshot", 'visuals/square3.png')
+        assert_image_approved("screenshot", 'visuals/square3.png',
+                              min_corr=0.45)
 
 
 @requires_application()
@@ -57,22 +56,22 @@ def test_rectangle_draw():
         polygon.transform = transforms.STTransform(scale=(200.0, 25),
                                                    translate=(50, 50))
         c.draw_visual(polygon)
-        assert_image_equal("screenshot", 'visuals/rectangle1.png')
+        assert_image_approved("screenshot", 'visuals/rectangle1.png')
 
-        gloo.clear()
         polygon = visuals.Polygon(pos=pos, color=(1, 1, 0, 1),
                                   border_color=(1, 0, 0, 1))
         polygon.transform = transforms.STTransform(scale=(200.0, 25),
                                                    translate=(50, 50))
         c.draw_visual(polygon)
-        assert_image_equal("screenshot", 'visuals/rectangle2.png')
+        assert_image_approved("screenshot", 'visuals/rectangle2.png')
 
-        gloo.clear()
-        polygon = visuals.Polygon(pos=pos, border_color=(1, 0, 0, 1))
+        polygon = visuals.Polygon(pos=pos, border_color=(1, 0, 0, 1),
+                                  border_width=1)
         polygon.transform = transforms.STTransform(scale=(200.0, 25),
-                                                   translate=(50, 50))
+                                                   translate=(50, 49))
         c.draw_visual(polygon)
-        assert_image_equal("screenshot", 'visuals/rectangle3.png')
+        assert_image_approved("screenshot", 'visuals/rectangle3.png',
+                              min_corr=0.7)
 
 
 @requires_application()
@@ -89,17 +88,18 @@ def test_reactive_draw():
                                                    translate=(50, 50))
         c.draw_visual(polygon)
 
-        gloo.clear()
         polygon.pos += [0.1, -0.1, 0]
         c.draw_visual(polygon)
-        assert_image_equal("screenshot", 'visuals/reactive_polygon1.png')
+        assert_image_approved("screenshot", 'visuals/reactive_polygon1.png')
 
-        gloo.clear()
         polygon.color = 'red'
         c.draw_visual(polygon)
-        assert_image_equal("screenshot", 'visuals/reactive_polygon2.png')
+        assert_image_approved("screenshot", 'visuals/reactive_polygon2.png')
 
-        gloo.clear()
         polygon.border_color = 'yellow'
         c.draw_visual(polygon)
-        assert_image_equal("screenshot", 'visuals/reactive_polygon3.png')
+        assert_image_approved("screenshot", 'visuals/reactive_polygon3.png',
+                              min_corr=0.8)
+
+
+run_tests_if_main()
diff --git a/vispy/visuals/tests/test_rectangle.py b/vispy/visuals/tests/test_rectangle.py
new file mode 100644
index 0000000..ce4ab3b
--- /dev/null
+++ b/vispy/visuals/tests/test_rectangle.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+
+"""
+Tests for RectPolygonVisual
+All images are of size (100,100) to keep a small file size
+"""
+
+from vispy.scene import visuals, transforms
+from vispy.testing import (requires_application, TestingCanvas,
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
+from pytest import raises
+
+
+ at requires_application()
+def test_rectangle_draw():
+    """Test drawing rectpolygons without transform using RectPolygonVisual"""
+    with TestingCanvas() as c:
+        rectpolygon = visuals.Rectangle(pos=(50, 50, 0), height=40.,
+                                        width=80., color='red')
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot", 'visuals/rectpolygon1.png')
+
+        rectpolygon = visuals.Rectangle(pos=(50, 50, 0), height=40.,
+                                        width=80., radius=10., color='red')
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot", 'visuals/rectpolygon2.png')
+
+        rectpolygon = visuals.Rectangle(pos=(50, 50, 0), height=40.,
+                                        width=80., radius=10., color='red',
+                                        border_color=(0, 1, 1, 1))
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot", 'visuals/rectpolygon3.png')
+
+        rectpolygon = visuals.Rectangle(pos=(50, 50, 0), height=40.,
+                                        width=80., radius=10.,
+                                        border_color='white')
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot", 'visuals/rectpolygon4.png', 
+                              min_corr=0.5)
+
+        rectpolygon = visuals.Rectangle(pos=(50, 50, 0), height=60.,
+                                        width=80., radius=[25, 10, 0, 15],
+                                        color='red', border_color=(0, 1, 1, 1))
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot", 'visuals/rectpolygon5.png')
+
+
+ at requires_application()
+def test_rectpolygon_draw():
+    """Test drawing transformed rectpolygons using RectPolygonVisual"""
+    with TestingCanvas() as c:
+        rectpolygon = visuals.Rectangle(pos=(0., 0.), height=20.,
+                                        width=20., radius=10., color='blue')
+        rectpolygon.transform = transforms.STTransform(scale=(2.0, 3.0),
+                                                       translate=(50, 50))
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot", 'visuals/rectpolygon6.png')
+
+        rectpolygon = visuals.Rectangle(pos=(0., 0.), height=20.,
+                                        width=20., radius=10.,
+                                        color='blue', border_color='red')
+        rectpolygon.transform = transforms.STTransform(scale=(2.0, 3.0),
+                                                       translate=(50, 50))
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot", 'visuals/rectpolygon7.png')
+
+        rectpolygon = visuals.Rectangle(pos=(0., 0.), height=60.,
+                                        width=60., radius=10.,
+                                        border_color='red')
+        rectpolygon.transform = transforms.STTransform(scale=(1.5, 0.5),
+                                                       translate=(50, 50))
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot", 'visuals/rectpolygon8.png', 
+                              min_corr=0.5)
+
+        rectpolygon = visuals.Rectangle(pos=(0., 0.), height=60.,
+                                        width=60., radius=[25, 10, 0, 15],
+                                        color='blue', border_color='red')
+        rectpolygon.transform = transforms.STTransform(scale=(1.5, 0.5),
+                                                       translate=(50, 50))
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot", 'visuals/rectpolygon9.png')
+
+
+ at requires_application()
+def test_reactive_draw():
+    """Test reactive RectPolygon attributes"""
+    with TestingCanvas() as c:
+        rectpolygon = visuals.Rectangle(pos=(50, 50, 0), height=40.,
+                                        width=80., color='red')
+        c.draw_visual(rectpolygon)
+
+        rectpolygon.radius = [20., 20, 0., 10.]
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot",
+                              'visuals/reactive_rectpolygon1.png')
+
+        rectpolygon.pos = (60, 60, 0)
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot",
+                              'visuals/reactive_rectpolygon2.png')
+
+        rectpolygon.color = 'blue'
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot",
+                              'visuals/reactive_rectpolygon3.png')
+
+        rectpolygon.border_color = 'yellow'
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot",
+                              'visuals/reactive_rectpolygon4.png')
+
+        rectpolygon.radius = 10.
+        c.draw_visual(rectpolygon)
+        assert_image_approved("screenshot",
+                              'visuals/reactive_rectpolygon5.png')
+
+
+ at requires_application()
+def test_attributes():
+    """Test if attribute checks are in place"""
+    with TestingCanvas():
+        rectpolygon = visuals.Rectangle(pos=(50, 50, 0), height=40.,
+                                        width=80., color='red')
+        with raises(ValueError):
+            rectpolygon.height = 0
+        with raises(ValueError):
+            rectpolygon.width = 0
+        with raises(ValueError):
+            rectpolygon.radius = [10, 0, 5]
+        with raises(ValueError):
+            rectpolygon.radius = [10.]
+        with raises(ValueError):
+            rectpolygon.radius = 21.
+
+
+run_tests_if_main()
diff --git a/vispy/scene/visuals/tests/test_regular_polygon.py b/vispy/visuals/tests/test_regular_polygon.py
similarity index 68%
rename from vispy/scene/visuals/tests/test_regular_polygon.py
rename to vispy/visuals/tests/test_regular_polygon.py
index 810490f..17f2063 100644
--- a/vispy/scene/visuals/tests/test_regular_polygon.py
+++ b/vispy/visuals/tests/test_regular_polygon.py
@@ -1,14 +1,16 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 """
 Tests for RegularPolygonVisual
 All images are of size (100,100) to keep a small file size
 """
 
-from vispy import gloo
 from vispy.scene import visuals, transforms
-from vispy.testing import (requires_application, assert_image_equal,
-                           TestingCanvas)
+from vispy.testing import (requires_application, TestingCanvas,
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
 
 
 @requires_application()
@@ -20,24 +22,23 @@ def test_regular_polygon_draw1():
         rpolygon.transform = transforms.STTransform(scale=(50, 50),
                                                     translate=(50, 50))
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot", 'visuals/regular_polygon1.png')
+        assert_image_approved("screenshot", 'visuals/regular_polygon1.png')
 
-        gloo.clear()
         rpolygon = visuals.RegularPolygon(pos=(0., 0.), radius=0.4, sides=8,
                                           color=(1, 0, 0, 1),
                                           border_color=(0, 1, 1, 1))
         rpolygon.transform = transforms.STTransform(scale=(50, 50),
                                                     translate=(50, 50))
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot", 'visuals/regular_polygon2.png')
+        assert_image_approved("screenshot", 'visuals/regular_polygon2.png')
 
-        gloo.clear()
         rpolygon = visuals.RegularPolygon(pos=(0., 0.), radius=0.4, sides=8,
                                           border_color=(0, 1, 1, 1))
         rpolygon.transform = transforms.STTransform(scale=(50, 50),
                                                     translate=(50, 50))
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot", 'visuals/regular_polygon3.png')
+        assert_image_approved("screenshot", 'visuals/regular_polygon3.png',
+                              min_corr=0.7)
 
 
 @requires_application()
@@ -49,24 +50,23 @@ def test_regular_polygon_draw2():
         rpolygon.transform = transforms.STTransform(scale=(75, 100),
                                                     translate=(50, 50))
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot", 'visuals/regular_polygon4.png')
+        assert_image_approved("screenshot", 'visuals/regular_polygon4.png')
 
-        gloo.clear()
         rpolygon = visuals.RegularPolygon(pos=(0., 0.), radius=0.4, sides=8,
                                           color=(0, 0, 1, 1),
                                           border_color=(1, 0, 0, 1))
         rpolygon.transform = transforms.STTransform(scale=(75, 100),
                                                     translate=(50, 50))
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot", 'visuals/regular_polygon5.png')
+        assert_image_approved("screenshot", 'visuals/regular_polygon5.png')
 
-        gloo.clear()
         rpolygon = visuals.RegularPolygon(pos=(0., 0.), radius=0.4, sides=8,
                                           border_color=(1, 0, 0, 1))
         rpolygon.transform = transforms.STTransform(scale=(75, 100),
                                                     translate=(50, 50))
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot", 'visuals/regular_polygon6.png')
+        assert_image_approved("screenshot", 'visuals/regular_polygon6.png', 
+                              min_corr=0.6)
 
 
 @requires_application()
@@ -77,32 +77,30 @@ def test_reactive_draw():
                                           color='yellow')
         c.draw_visual(rpolygon)
 
-        gloo.clear()
         rpolygon.pos = [70, 40, 0.]
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot",
-                           'visuals/reactive_regular_polygon1.png')
+        assert_image_approved("screenshot",
+                              'visuals/reactive_regular_polygon1.png')
 
-        gloo.clear()
         rpolygon.radius = 25
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot",
-                           'visuals/reactive_regular_polygon2.png')
+        assert_image_approved("screenshot",
+                              'visuals/reactive_regular_polygon2.png')
 
-        gloo.clear()
         rpolygon.color = 'red'
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot",
-                           'visuals/reactive_regular_polygon3.png')
+        assert_image_approved("screenshot",
+                              'visuals/reactive_regular_polygon3.png')
 
-        gloo.clear()
         rpolygon.border_color = 'yellow'
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot",
-                           'visuals/reactive_regular_polygon4.png')
+        assert_image_approved("screenshot",
+                              'visuals/reactive_regular_polygon4.png')
 
-        gloo.clear()
         rpolygon.sides = 6
         c.draw_visual(rpolygon)
-        assert_image_equal("screenshot",
-                           'visuals/reactive_regular_polygon5.png')
+        assert_image_approved("screenshot",
+                              'visuals/reactive_regular_polygon5.png')
+
+
+run_tests_if_main()
diff --git a/vispy/scene/visuals/tests/test_sdf.py b/vispy/visuals/tests/test_sdf.py
similarity index 77%
rename from vispy/scene/visuals/tests/test_sdf.py
rename to vispy/visuals/tests/test_sdf.py
index 77d5840..8d7c1d2 100644
--- a/vispy/scene/visuals/tests/test_sdf.py
+++ b/vispy/visuals/tests/test_sdf.py
@@ -1,16 +1,18 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import numpy as np
 from numpy.testing import assert_allclose
 
 from vispy.app import Canvas
-from vispy.scene.visuals.text._sdf import SDFRenderer
+from vispy.visuals.text._sdf import SDFRenderer
 from vispy import gloo
-from vispy.testing import requires_application
+from vispy.testing import requires_application, run_tests_if_main
 
 
 @requires_application()
-def test_text():
-    """Test basic text support"""
+def test_sdf():
+    """Test basic text support - sdf"""
     # test a simple cases
     data = (np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
                       [0, 0, 0, 0, 0, 0, 0, 0, 0],
@@ -28,8 +30,7 @@ def test_text():
     expd = np.round(256 * expd).astype(np.int)
 
     with Canvas(size=(100, 100)):
-        tex = gloo.Texture2D(shape=data.shape + (3,), dtype=np.ubyte,
-                             format='rgb')
+        tex = gloo.Texture2D(data.shape + (3,), format='rgb')
         SDFRenderer().render_to_texture(data, tex, (0, 0), data.shape[::-1])
         gloo.set_viewport(0, 0, *data.shape[::-1])
         gloo.util.draw_texture(tex)
@@ -37,3 +38,6 @@ def test_text():
         print(result)
         print(expd)
         assert_allclose(result, expd, atol=1)
+
+
+run_tests_if_main()
diff --git a/vispy/visuals/tests/test_spectrogram.py b/vispy/visuals/tests/test_spectrogram.py
new file mode 100644
index 0000000..754dc4d
--- /dev/null
+++ b/vispy/visuals/tests/test_spectrogram.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+import numpy as np
+
+from vispy.scene.visuals import Spectrogram
+from vispy.testing import (requires_application, TestingCanvas,
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
+
+
+ at requires_application()
+def test_spectrogram():
+    """Test spectrogram visual"""
+    n_fft = 256
+    n_freqs = n_fft // 2 + 1
+    size = (100, n_freqs)
+    with TestingCanvas(size=size) as c:
+        np.random.seed(67853498)
+        data = np.random.normal(size=n_fft * 100)
+        spec = Spectrogram(data, n_fft=n_fft, step=n_fft, window=None,
+                           color_scale='linear', cmap='grays')
+        c.draw_visual(spec)
+        #expected = np.zeros(size[::-1] + (3,))
+        #expected[0] = 1.
+        assert_image_approved("screenshot", "visuals/spectrogram.png")
+        freqs = spec.freqs
+        assert len(freqs) == n_freqs
+        assert freqs[0] == 0
+        assert freqs[-1] == 0.5
+
+run_tests_if_main()
diff --git a/vispy/scene/visuals/tests/test_text.py b/vispy/visuals/tests/test_text.py
similarity index 56%
rename from vispy/scene/visuals/tests/test_text.py
rename to vispy/visuals/tests/test_text.py
index 2d9022d..c6aba66 100644
--- a/vispy/scene/visuals/tests/test_text.py
+++ b/vispy/visuals/tests/test_text.py
@@ -1,19 +1,21 @@
 # -*- coding: utf-8 -*-
 from vispy.scene.visuals import Text
 from vispy.testing import (requires_application, TestingCanvas,
-                           assert_image_equal)
+                           run_tests_if_main)
+from vispy.testing.image_tester import assert_image_approved
 
 
 @requires_application()
 def test_text():
     """Test basic text support"""
-    with TestingCanvas(bgcolor='w', size=(92, 92)) as c:
+    
+    with TestingCanvas(bgcolor='w', size=(92, 92), dpi=92) as c:
         pos = [92 // 2] * 2
         text = Text('testing', font_size=20, color='k',
                     pos=pos, anchor_x='center', anchor_y='baseline')
         c.draw_visual(text)
-        # This limit seems large, but the images actually match quite well...
-        # TODO: we should probably make more "modes" for assert_image_equal
-        # at some point
         # Test image created in Illustrator CS5, 1"x1" output @ 92 DPI
-        assert_image_equal("screenshot", 'visuals/text1.png', limit=840)
+        assert_image_approved("screenshot", 'visuals/text1.png')
+
+
+run_tests_if_main()
diff --git a/vispy/visuals/tests/test_volume.py b/vispy/visuals/tests/test_volume.py
new file mode 100644
index 0000000..c30e955
--- /dev/null
+++ b/vispy/visuals/tests/test_volume.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+
+import numpy as np
+from pytest import raises
+from vispy import scene
+
+from vispy.testing import (TestingCanvas, requires_application,
+                           run_tests_if_main, requires_pyopengl)
+from vispy.testing.image_tester import assert_image_approved
+
+
+ at requires_pyopengl()
+def test_volume():
+    
+    vol = np.zeros((20, 20, 20), 'float32')
+    vol[8:16, 8:16, :] = 1.0
+    
+    # Create
+    V = scene.visuals.Volume(vol)
+    assert V.clim == (0, 1)
+    assert V.method == 'mip'
+    
+    # Set wrong data
+    with raises(ValueError):
+        V.set_data(np.zeros((20, 20), 'float32'))
+    
+    # Clim
+    V.set_data(vol, (0.5, 0.8))
+    assert V.clim == (0.5, 0.8)
+    with raises(ValueError):
+        V.set_data(vol, (0.5, 0.8, 1.0))
+    
+    # Method
+    V.method = 'iso'
+    assert V.method == 'iso'
+    
+    # Step size
+    V.relative_step_size = 1.1
+    assert V.relative_step_size == 1.1
+    # Disallow 0 step size to avoid GPU stalling
+    with raises(ValueError):
+        V.relative_step_size = 0
+
+
+ at requires_pyopengl()
+ at requires_application()
+def test_volume_draw():
+    with TestingCanvas(bgcolor='k', size=(100, 100)) as c:
+        v = c.central_widget.add_view()
+        v.camera = 'turntable'
+        v.camera.fov = 70
+        # Create
+        np.random.seed(2376)
+        vol = np.random.normal(size=(20, 20, 20), loc=0.5, scale=0.2)
+        vol[8:16, 8:16, :] += 1.0
+        V = scene.visuals.Volume(vol, parent=v.scene)  # noqa
+        v.camera.set_range()
+        assert_image_approved(c.render(), 'visuals/volume.png')
+
+
+run_tests_if_main()
diff --git a/vispy/scene/visuals/text/__init__.py b/vispy/visuals/text/__init__.py
similarity index 72%
rename from vispy/scene/visuals/text/__init__.py
rename to vispy/visuals/text/__init__.py
index 2474929..7bd36f2 100644
--- a/vispy/scene/visuals/text/__init__.py
+++ b/vispy/visuals/text/__init__.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
-from .text import Text  # noqa
+from .text import TextVisual  # noqa
diff --git a/vispy/scene/visuals/text/_sdf.py b/vispy/visuals/text/_sdf.py
similarity index 92%
rename from vispy/scene/visuals/text/_sdf.py
rename to vispy/visuals/text/_sdf.py
index e08db66..b2edc70 100644
--- a/vispy/scene/visuals/text/_sdf.py
+++ b/vispy/visuals/text/_sdf.py
@@ -9,11 +9,23 @@ Adapted to `vispy` by Eric Larson <larson.eric.d at gmail.com>.
 
 import numpy as np
 from os import path as op
-from ....gloo import (Program, VertexShader, FragmentShader, FrameBuffer,
-                      VertexBuffer, Texture2D, set_viewport, set_state)
+from ...gloo import (Program, FrameBuffer, VertexBuffer, Texture2D, 
+                     set_viewport, set_state)
 
 this_dir = op.dirname(__file__)
 
+vert_seed = """
+attribute vec2 a_position;
+attribute vec2 a_texcoord;
+varying vec2 v_uv;
+
+void main( void )
+{
+  v_uv = a_texcoord.xy;
+  gl_Position = vec4(a_position.xy, 0., 1.);
+}
+"""
+
 vert = """
 uniform float u_texw;
 uniform float u_texh;
@@ -219,10 +231,9 @@ void main( void )
 
 class SDFRenderer(object):
     def __init__(self):
-        vert_shader = VertexShader(vert)
-        self.program_seed = Program(vert_shader, FragmentShader(frag_seed))
-        self.program_flood = Program(vert_shader, FragmentShader(frag_flood))
-        self.program_insert = Program(vert_shader, FragmentShader(frag_insert))
+        self.program_seed = Program(vert_seed, frag_seed)
+        self.program_flood = Program(vert, frag_flood)
+        self.program_insert = Program(vert, frag_insert)
         self.programs = [self.program_seed, self.program_flood,
                          self.program_insert]
 
@@ -255,13 +266,13 @@ class SDFRenderer(object):
         set_state(blend=False, depth_test=False)
 
         # calculate the negative half (within object)
-        orig_tex = Texture2D(255 - data, format='luminance')
-        orig_tex.wrapping = 'clamp_to_edge'
-        orig_tex.interpolation = 'nearest'
+        orig_tex = Texture2D(255 - data, format='luminance', 
+                             wrapping='clamp_to_edge', interpolation='nearest')
         edf_neg_tex = self._render_edf(orig_tex)
 
         # calculate positive half (outside object)
         orig_tex[:, :, 0] = data
+        
         edf_pos_tex = self._render_edf(orig_tex)
 
         # render final product to output texture
@@ -280,14 +291,12 @@ class SDFRenderer(object):
 
         comp_texs = []
         for _ in range(2):
-            tex = Texture2D(shape=sdf_size + (4,), dtype=np.float32,
-                            format='rgba')
-            tex.interpolation = 'nearest'
-            tex.wrapping = 'clamp_to_edge'
+            tex = Texture2D(sdf_size + (4,), format='rgba',
+                            interpolation='nearest', wrapping='clamp_to_edge')
             comp_texs.append(tex)
         self.fbo_to[0].color_buffer = comp_texs[0]
         self.fbo_to[1].color_buffer = comp_texs[1]
-        for program in self.programs:
+        for program in self.programs[1:]:  # program_seed does not need this
             program['u_texh'], program['u_texw'] = sdf_size
 
         # Do the rendering
diff --git a/vispy/scene/visuals/text/text.py b/vispy/visuals/text/text.py
similarity index 86%
rename from vispy/scene/visuals/text/text.py
rename to vispy/visuals/text/text.py
index cc4ac5d..f17080f 100644
--- a/vispy/scene/visuals/text/text.py
+++ b/vispy/visuals/text/text.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
+# Copyright (c) 2015, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
 
@@ -13,17 +13,20 @@ from __future__ import division
 import numpy as np
 from copy import deepcopy
 from os import path as op
+import sys
 
 from ._sdf import SDFRenderer
-from ....gloo import (TextureAtlas, set_state, IndexBuffer, VertexBuffer,
-                      set_viewport, get_parameter)
-from ....gloo.wrappers import _check_valid
-from ....ext.six import string_types
-from ....util.fonts import _load_glyph
-from ...shaders import ModularProgram
-from ....color import Color
+from ...gloo import (TextureAtlas, set_state, IndexBuffer, VertexBuffer,
+                     set_viewport)
+from ...gloo import gl
+from ...gloo.wrappers import _check_valid
+from ...ext.six import string_types
+from ...util.fonts import _load_glyph
+from ..transforms import STTransform
+from ..shaders import ModularProgram
+from ...color import Color
 from ..visual import Visual
-from ....io import _data_dir
+from ...io import _data_dir
 
 
 class TextureFont(object):
@@ -72,8 +75,8 @@ class TextureFont(object):
     def _load_char(self, char):
         """Build and store a glyph corresponding to an individual character
 
-        Parameters:
-        -----------
+        Parameters
+        ----------
         char : str
             A single character to be represented.
         """
@@ -138,9 +141,14 @@ def _text_to_vbo(text, font, anchor_x, anchor_y, lowres_size):
     width = height = ascender = descender = 0
     ratio, slop = 1. / font.ratio, font.slop
     x_off = -slop
+    # Need to make sure we have a unicode string here (Py2.7 mis-interprets
+    # characters like "•" otherwise)
+    if sys.version[0] == '2' and isinstance(text, str):
+        text = text.decode('utf-8')
     # Need to store the original viewport, because the font[char] will
     # trigger SDF rendering, which changes our viewport
-    orig_viewport = get_parameter('viewport')
+    # todo: get rid of call to glGetParameter!
+    orig_viewport = gl.glGetParameter(gl.GL_VIEWPORT)
     for ii, char in enumerate(text):
         glyph = font[char]
         kerning = glyph['kerning'].get(prev, 0.) * ratio
@@ -193,7 +201,7 @@ def _text_to_vbo(text, font, anchor_x, anchor_y, lowres_size):
     return VertexBuffer(vertices)
 
 
-class Text(Visual):
+class TextVisual(Visual):
     """Visual that displays text
 
     Parameters
@@ -218,24 +226,25 @@ class Text(Visual):
         Horizontal text anchor.
     anchor_y : str
         Vertical text anchor.
-    parent : instance of Entity
-        The parent of the Text visual.
+    font_manager : object | None
+        Font manager to use (can be shared if the GLContext is shared).
     """
 
     VERTEX_SHADER = """
-        uniform vec2 u_pos;  // anchor position
-        uniform vec2 u_scale;  // to scale to pixel units
+        uniform vec3 u_pos;  // anchor position
         uniform float u_rotation;  // rotation in rad
         attribute vec2 a_position; // in point units
         attribute vec2 a_texcoord;
-
         varying vec2 v_texcoord;
 
         void main(void) {
-            vec4 pos = $transform(vec4(u_pos, 0.0, 1.0));
-            mat2 rot = mat2(cos(u_rotation), -sin(u_rotation),
-                            sin(u_rotation), cos(u_rotation));
-            gl_Position = pos + vec4(rot * a_position * u_scale, 0., 0.);
+            // Eventually "rot" should be handled by SRTTransform or so...
+            mat4 rot = mat4(cos(u_rotation), -sin(u_rotation), 0, 0,
+                            sin(u_rotation), cos(u_rotation), 0, 0,
+                            0, 0, 1, 0, 0, 0, 0, 1);
+            vec4 pos = $transform(vec4(u_pos, 1.0)) +
+                       $text_scale(rot * vec4(a_position, 0, 0));
+            gl_Position = pos;
             v_texcoord = a_texcoord;
         }
         """
@@ -356,12 +365,11 @@ class Text(Visual):
         """
 
     def __init__(self, text, color='black', bold=False,
-                 italic=False, face='OpenSans', font_size=12, pos=(0, 0),
+                 italic=False, face='OpenSans', font_size=12, pos=[0, 0, 0],
                  rotation=0., anchor_x='center', anchor_y='center',
                  font_manager=None, **kwargs):
         Visual.__init__(self, **kwargs)
         # Check input
-        assert isinstance(text, string_types)
         valid_keys = ('top', 'center', 'middle', 'baseline', 'bottom')
         _check_valid('anchor_y', anchor_y, valid_keys)
         valid_keys = ('left', 'center', 'right')
@@ -380,6 +388,7 @@ class Text(Visual):
         self.font_size = font_size
         self.pos = pos
         self.rotation = rotation
+        self._text_scale = STTransform()
 
     @property
     def text(self):
@@ -430,17 +439,27 @@ class Text(Visual):
 
     @pos.setter
     def pos(self, pos):
-        pos = [float(p) for p in pos]
-        assert len(pos) == 2
-        self._pos = tuple(pos)
-
-    def draw(self, event=None):
+        self._pos = np.array(pos, np.float32)
+        if self._pos.ndim != 1 or self._pos.size not in (2, 3):
+            raise ValueError('pos must be array-like with 2 or 3 elements')
+        if self._pos.size == 2:
+            self._pos = np.concatenate((self._pos, [0.]))
+
+    def draw(self, transforms):
+        """Draw the Text
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
         # attributes / uniforms are not available until program is built
         if len(self.text) == 0:
             return
         if self._vertices is None:
             # we delay creating vertices because it requires a context,
             # which may or may not exist when the object is initialized
+            transforms.canvas.context.flush_commands()  # flush GLIR commands
             self._vertices = _text_to_vbo(self._text, self._font,
                                           self._anchors[0], self._anchors[1],
                                           self._font._lowres_size)
@@ -448,28 +467,23 @@ class Text(Visual):
                    np.arange(0, 4*len(self._text), 4,
                              dtype=np.uint32)[:, np.newaxis])
             self._ib = IndexBuffer(idx.ravel())
+            self._program.bind(self._vertices)
 
-        if event is not None:
-            xform = event.render_transform.shader_map()
-            self._program.vert['transform'] = xform
-            px_scale = event.framebuffer_cs.transform.scale
-        else:
-            self._program.vert['transform'] = self.transform.shader_map()
-            # Rather arbitrary scale. With size=12 it takes up ~1/10 of space
-            px_scale = 0.01, 0.01
-
-        self._program.prepare()  # Force ModularProgram to set shaders
         # todo: do some testing to verify that the scaling is correct
-        ps = (self._font_size / 72.) * 92.
-        self._program['u_npix'] = ps
-        self._program['u_font_atlas_shape'] = self._font._atlas.shape[:2]
+        n_pix = (self._font_size / 72.) * transforms.dpi  # logical pix
+        tr = (transforms.document_to_framebuffer *
+              transforms.framebuffer_to_render)
+        px_scale = (tr.map((1, 0)) - tr.map((0, 1)))[:2]
+        self._program.vert['transform'] = transforms.get_full_transform()
+        self._text_scale.scale = px_scale * n_pix
+        self._program.vert['text_scale'] = self._text_scale
+        self._program['u_npix'] = n_pix
         self._program['u_kernel'] = self._font._kernel
-        self._program['u_scale'] = ps * px_scale[0], ps * px_scale[1]
         self._program['u_rotation'] = self._rotation
         self._program['u_pos'] = self._pos
         self._program['u_color'] = self._color.rgba
         self._program['u_font_atlas'] = self._font._atlas
-        self._program.bind(self._vertices)
+        self._program['u_font_atlas_shape'] = self._font._atlas.shape[:2]
         set_state(blend=True, depth_test=False,
                   blend_func=('src_alpha', 'one_minus_src_alpha'))
         self._program.draw('triangles', self._ib)
diff --git a/vispy/scene/transforms/__init__.py b/vispy/visuals/transforms/__init__.py
similarity index 70%
rename from vispy/scene/transforms/__init__.py
rename to vispy/visuals/transforms/__init__.py
index a54e1ac..870b41a 100644
--- a/vispy/scene/transforms/__init__.py
+++ b/vispy/visuals/transforms/__init__.py
@@ -1,22 +1,24 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 """
 Provides classes representing different transform types suitable for
 use with visuals and scenes.
 """
 
-__all__ = ['NullTransform', 'STTransform', 'AffineTransform',
-           'PerspectiveTransform', 'LogTransform', 'PolarTransform',
-           'ChainTransform']
 
 from .base_transform import BaseTransform  # noqa
 from .linear import (NullTransform, STTransform,  # noqa
                      AffineTransform,  PerspectiveTransform)  # noqa
 from .nonlinear import LogTransform, PolarTransform  # noqa
+from .interactive import PanZoomTransform
 from .chain import ChainTransform  # noqa
-from ._util import arg_to_array, arg_to_vec4, TransformCache  # noqa
+from ._util import arg_to_array, arg_to_vec4, as_vec4, TransformCache  # noqa
+from .transform_system import TransformSystem
 
+__all__ = ['NullTransform', 'STTransform', 'AffineTransform',
+           'PerspectiveTransform', 'LogTransform', 'PolarTransform',
+           'ChainTransform', 'TransformSystem', 'PanZoomTransform']
 
 transform_types = {}
 for o in list(globals().values()):
@@ -28,5 +30,5 @@ for o in list(globals().values()):
         continue
 
 
-def create_transform(type, *args, **kwds):
-    return transform_types[type](*args, **kwds)
+def create_transform(type, *args, **kwargs):
+    return transform_types[type](*args, **kwargs)
diff --git a/vispy/scene/transforms/_util.py b/vispy/visuals/transforms/_util.py
similarity index 61%
rename from vispy/scene/transforms/_util.py
rename to vispy/visuals/transforms/_util.py
index 6124c19..7491166 100644
--- a/vispy/scene/transforms/_util.py
+++ b/vispy/visuals/transforms/_util.py
@@ -1,35 +1,75 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
 
 import numpy as np
+from ...ext.decorator import decorator
+from ...util import logger
 
 
 def arg_to_array(func):
     """
     Decorator to convert argument to array.
+
+    Parameters
+    ----------
+    func : function
+        The function to decorate.
+
+    Returns
+    -------
+    func : function
+        The decorated function.
     """
-    def fn(self, arg, *args, **kwds):
-        return func(self, np.array(arg), *args, **kwds)
+    def fn(self, arg, *args, **kwargs):
+        """Function
+
+        Parameters
+        ----------
+        arg : array-like
+            Argument to convert.
+        *args : tuple
+            Arguments.
+        **kwargs : dict
+            Keyword arguments.
+
+        Returns
+        -------
+        value : object
+            The return value of the function.
+        """
+        return func(self, np.array(arg), *args, **kwargs)
     return fn
 
 
 def as_vec4(obj, default=(0, 0, 0, 1)):
     """
-    Convert *obj* to 4-element vector (numpy array with shape[-1] == 4)
+    Convert `obj` to 4-element vector (numpy array with shape[-1] == 4)
+
+    Parameters
+    ----------
+    obj : array-like
+        Original object.
+    default : array-like
+        The defaults to use if the object does not have 4 entries.
 
-    If *obj* has < 4 elements, then new elements are added from *default*.
+    Returns
+    -------
+    obj : array-like
+        The object promoted to have 4 elements.
+
+    Notes
+    -----
+    `obj` will have at least two dimensions.
+
+    If `obj` has < 4 elements, then new elements are added from `default`.
     For inputs intended as a position or translation, use default=(0,0,0,1).
     For inputs intended as scale factors, use default=(1,1,1,1).
-    """
-    obj = np.array(obj)
-
-    # If this is a single vector, reshape to (1, 4)
-    if obj.ndim == 1:
-        obj = obj[np.newaxis, :]
 
+    """
+    obj = np.atleast_2d(obj)
     # For multiple vectors, reshape to (..., 4)
     if obj.shape[-1] < 4:
         new = np.empty(obj.shape[:-1] + (4,), dtype=obj.dtype)
@@ -39,11 +79,11 @@ def as_vec4(obj, default=(0, 0, 0, 1)):
     elif obj.shape[-1] > 4:
         raise TypeError("Array shape %s cannot be converted to vec4"
                         % obj.shape)
-
     return obj
 
 
-def arg_to_vec4(func):
+ at decorator
+def arg_to_vec4(func, self_, arg, *args, **kwargs):
     """
     Decorator for converting argument to vec4 format suitable for 4x4 matrix
     multiplication.
@@ -65,28 +105,25 @@ def arg_to_vec4(func):
     and returns a new (mapped) object.
 
     """
-    def fn(self, arg, *args, **kwds):
-        if isinstance(arg, (tuple, list, np.ndarray)):
-            arg = np.array(arg)
-            flatten = arg.ndim == 1
-            arg = as_vec4(arg)
-
-            ret = func(self, arg, *args, **kwds)
-            if flatten and ret is not None:
-                return ret.flatten()
-            return ret
-        elif hasattr(arg, '_transform_in'):
-            arr = arg._transform_in()
-            ret = func(self, arr, *args, **kwds)
-            return arg._transform_out(ret)
-        else:
-            raise TypeError("Cannot convert argument to 4D vector: %s" % arg)
-    return fn
+    if isinstance(arg, (tuple, list, np.ndarray)):
+        arg = np.array(arg)
+        flatten = arg.ndim == 1
+        arg = as_vec4(arg)
+
+        ret = func(self_, arg, *args, **kwargs)
+        if flatten and ret is not None:
+            return ret.flatten()
+        return ret
+    elif hasattr(arg, '_transform_in'):
+        arr = arg._transform_in()
+        ret = func(self_, arr, *args, **kwargs)
+        return arg._transform_out(ret)
+    else:
+        raise TypeError("Cannot convert argument to 4D vector: %s" % arg)
 
 
 class TransformCache(object):
-    """ Utility class for managing a cache of transforms that map along an
-    Entity path.
+    """ Utility class for managing a cache of ChainTransforms.
 
     This is an LRU cache; items are removed if they are not accessed after
     *max_age* calls to roll().
@@ -113,15 +150,16 @@ class TransformCache(object):
         key = tuple(map(id, path))
         item = self._cache.get(key, None)
         if item is None:
+            logger.debug("Transform cache miss: %s", key)
             item = [0, self._create(path)]
             self._cache[key] = item
         item[0] = 0  # reset age for this item
 
         # make sure the chain is up to date
         #tr = item[1]
-        #for i, entity in enumerate(path[1:]):
-        #    if tr.transforms[i] is not entity.transform:
-        #        tr[i] = entity.transform
+        #for i, node in enumerate(path[1:]):
+        #    if tr.transforms[i] is not node.transform:
+        #        tr[i] = node.transform
 
         return item[1]
 
@@ -141,4 +179,5 @@ class TransformCache(object):
             item[0] += 1
 
         for key in rem:
+            logger.debug("TransformCache remove: %s", key)
             del self._cache[key]
diff --git a/vispy/scene/transforms/base_transform.py b/vispy/visuals/transforms/base_transform.py
similarity index 86%
rename from vispy/scene/transforms/base_transform.py
rename to vispy/visuals/transforms/base_transform.py
index 536fa82..c70cf98 100644
--- a/vispy/scene/transforms/base_transform.py
+++ b/vispy/visuals/transforms/base_transform.py
@@ -1,12 +1,7 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
-from __future__ import division
-
-from ..shaders import Function
-from ...util.event import EventEmitter
-
 """
 API Issues to work out:
 
@@ -21,6 +16,11 @@ API Issues to work out:
     rect.
 """
 
+from __future__ import division
+
+from ..shaders import Function
+from ...util.event import EventEmitter
+
 
 class BaseTransform(object):
     """
@@ -76,7 +76,8 @@ class BaseTransform(object):
         """
         Return *obj* mapped through the forward transformation.
 
-        Parameters:
+        Parameters
+        ----------
             obj : tuple (x,y) or (x,y,z)
                   array with shape (..., 2) or (..., 3)
         """
@@ -86,7 +87,8 @@ class BaseTransform(object):
         """
         Return *obj* mapped through the inverse transformation.
 
-        Parameters:
+        Parameters
+        ----------
             obj : tuple (x,y) or (x,y,z)
                   array with shape (..., 2) or (..., 3)
         """
@@ -114,6 +116,19 @@ class BaseTransform(object):
         """
         return self._shader_imap
 
+    def _shader_object(self):
+        """ This method allows transforms to be assigned directly to shader
+        template variables. 
+        
+        Example::
+        
+            code = 'void main() { gl_Position = $transform($position); }'
+            func = shaders.Function(code)
+            tr = STTransform()
+            func['transform'] = tr  # use tr's forward mapping for $function
+        """
+        return self.shader_map()
+
     def update(self):
         """
         Called to inform any listeners that this transform has changed.
@@ -175,9 +190,25 @@ class InverseTransform(BaseTransform):
     def inverse(self):
         return self._transform
     
+    @property
+    def Linear(self):
+        return self._transform.Linear
+
+    @property
+    def Orthogonal(self):
+        return self._transform.Orthogonal
+
+    @property
+    def NonScaling(self):
+        return self._transform.NonScaling
+
+    @property
+    def Isometric(self):
+        return self._transform.Isometric
+    
     def __repr__(self):
         return ("<Inverse of %r>" % repr(self._transform))
         
 
 # import here to avoid import cycle; needed for BaseTransform.__mul__.
-from .chain import ChainTransform
+from .chain import ChainTransform  # noqa
diff --git a/vispy/scene/transforms/chain.py b/vispy/visuals/transforms/chain.py
similarity index 80%
rename from vispy/scene/transforms/chain.py
rename to vispy/visuals/transforms/chain.py
index bdfc306..621086c 100644
--- a/vispy/scene/transforms/chain.py
+++ b/vispy/visuals/transforms/chain.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from __future__ import division
@@ -18,6 +18,7 @@ class ChainTransform(BaseTransform):
     Arguments:
 
     transforms : list of BaseTransform instances
+        See ``transforms`` property.
     """
     glsl_map = None
     glsl_imap = None
@@ -45,7 +46,23 @@ class ChainTransform(BaseTransform):
 
     @property
     def transforms(self):
-        """ Get the list of transform that make up the transform chain.
+        """ The list of transform that make up the transform chain.
+        
+        The order of transforms is given such that the last transform in the 
+        list is the first to be invoked when mapping coordinates through 
+        the chain. 
+        
+        For example, the following two mappings are equivalent::
+        
+            # Map coordinates through individual transforms:
+            trans1 = STTransform(scale=(2, 3), translate=(0, 1))
+            trans2 = PolarTransform()
+            mapped = trans1.map(trans2.map(coords))
+            
+            # Equivalent mapping through chain:
+            chain = ChainTransform([trans1, trans2])
+            mapped = chain.map(coords)
+            
         """
         return self._transforms
 
@@ -85,15 +102,39 @@ class ChainTransform(BaseTransform):
             b &= tr.Isometric
         return b
 
-    def map(self, obj):
+    def map(self, coords):
+        """Map coordinates
+
+        Parameters
+        ----------
+        coords : array-like
+            Coordinates to map.
+
+        Returns
+        -------
+        coords : ndarray
+            Coordinates.
+        """
         for tr in reversed(self.transforms):
-            obj = tr.map(obj)
-        return obj
+            coords = tr.map(coords)
+        return coords
 
-    def imap(self, obj):
+    def imap(self, coords):
+        """Inverse map coordinates
+
+        Parameters
+        ----------
+        coords : array-like
+            Coordinates to inverse map.
+
+        Returns
+        -------
+        coords : ndarray
+            Coordinates.
+        """
         for tr in self.transforms:
-            obj = tr.imap(obj)
-        return obj
+            coords = tr.imap(coords)
+        return coords
 
     def shader_map(self):
         if self._shader_map is None:
@@ -167,6 +208,11 @@ class ChainTransform(BaseTransform):
     def append(self, tr):
         """
         Add a new transform to the end of this chain.
+
+        Parameters
+        ----------
+        tr : instance of Transform
+            The transform to use.
         """
         self.transforms.append(tr)
         self.update()
@@ -187,6 +233,11 @@ class ChainTransform(BaseTransform):
     def prepend(self, tr):
         """
         Add a new transform to the beginning of this chain.
+
+        Parameters
+        ----------
+        tr : instance of Transform
+            The transform to use.
         """
         self.transforms.insert(0, tr)
         self.update()
diff --git a/vispy/visuals/transforms/interactive.py b/vispy/visuals/transforms/interactive.py
new file mode 100644
index 0000000..0191cff
--- /dev/null
+++ b/vispy/visuals/transforms/interactive.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+from __future__ import division
+
+import numpy as np
+from .linear import STTransform
+
+
+class PanZoomTransform(STTransform):
+    """Pan-zoom transform
+
+    Parameters
+    ----------
+    canvas : instance of Canvas | None
+        The canvas to attch to.
+    aspect : float | None
+        The aspect ratio to apply.
+    **kwargs : dict
+        Keyword arguments to pass to the underlying `STTransform`.
+    """
+    def __init__(self, canvas=None, aspect=None, **kwargs):
+        self._aspect = aspect
+        self.attach(canvas)
+        STTransform.__init__(self, **kwargs)
+        self.on_resize(None)
+        
+    def attach(self, canvas):
+        """Attach this tranform to a canvas
+
+        Parameters
+        ----------
+        canvas : instance of Canvas
+            The canvas.
+        """
+        self._canvas = canvas
+        canvas.events.resize.connect(self.on_resize)
+        canvas.events.mouse_wheel.connect(self.on_mouse_wheel)
+        canvas.events.mouse_move.connect(self.on_mouse_move)
+        
+    @property
+    def canvas_tr(self):
+        return STTransform.from_mapping(
+            [(0, 0), self._canvas.size],
+            [(-1, 1), (1, -1)])
+        
+    def on_resize(self, event):
+        """Resize handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        if self._aspect is None:
+            return
+        w, h = self._canvas.size
+        aspect = self._aspect / (w / h)
+        self.scale = (self.scale[0], self.scale[0] / aspect)
+        self.shader_map()
+
+    def on_mouse_move(self, event):
+        """Mouse move handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        if event.is_dragging:
+            dxy = event.pos - event.last_event.pos
+            button = event.press_event.button
+
+            if button == 1:
+                dxy = self.canvas_tr.map(dxy)
+                o = self.canvas_tr.map([0, 0])
+                t = dxy - o
+                self.move(t)
+            elif button == 2:
+                center = self.canvas_tr.map(event.press_event.pos)
+                if self._aspect is None:
+                    self.zoom(np.exp(dxy * (0.01, -0.01)), center)
+                else:
+                    s = dxy[1] * -0.01
+                    self.zoom(np.exp(np.array([s, s])), center)
+                    
+            self.shader_map()
+
+    def on_mouse_wheel(self, event):
+        """Mouse wheel handler
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        self.zoom(np.exp(event.delta * (0.01, -0.01)), event.pos)
diff --git a/vispy/visuals/transforms/linear.py b/vispy/visuals/transforms/linear.py
new file mode 100644
index 0000000..1953a6a
--- /dev/null
+++ b/vispy/visuals/transforms/linear.py
@@ -0,0 +1,575 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division
+
+import numpy as np
+
+from ...util import transforms
+from ...geometry import Rect
+from ._util import arg_to_vec4, as_vec4
+from .base_transform import BaseTransform
+
+
+class NullTransform(BaseTransform):
+    """ Transform having no effect on coordinates (identity transform).
+    """
+    glsl_map = "vec4 null_transform_map(vec4 pos) {return pos;}"
+    glsl_imap = "vec4 null_transform_imap(vec4 pos) {return pos;}"
+
+    Linear = True
+    Orthogonal = True
+    NonScaling = True
+    Isometric = True
+
+    @arg_to_vec4
+    def map(self, coords):
+        """Map coordinates
+
+        Parameters
+        ----------
+        coords : array-like
+            Coordinates to map.
+        """
+        return coords
+
+    def imap(self, coords):
+        """Inverse map coordinates
+
+        Parameters
+        ----------
+        coords : array-like
+            Coordinates to inverse map.
+        """
+        return coords
+
+    def __mul__(self, tr):
+        return tr
+
+    def __rmul__(self, tr):
+        return tr
+
+
+class STTransform(BaseTransform):
+    """ Transform performing only scale and translate, in that order.
+
+    Parameters
+    ----------
+    scale : array-like
+        Scale factors for X, Y, Z axes.
+    translate : array-like
+        Scale factors for X, Y, Z axes.
+    """
+    glsl_map = """
+        vec4 st_transform_map(vec4 pos) {
+            return vec4(pos.xyz * $scale.xyz + $translate.xyz * pos.w, pos.w);
+        }
+    """
+
+    glsl_imap = """
+        vec4 st_transform_imap(vec4 pos) {
+            return vec4((pos.xyz - $translate.xyz * pos.w) / $scale.xyz,
+                        pos.w);
+        }
+    """
+
+    Linear = True
+    Orthogonal = True
+    NonScaling = False
+    Isometric = False
+
+    def __init__(self, scale=None, translate=None):
+        super(STTransform, self).__init__()
+
+        self._update_map = True
+        self._update_imap = True
+        self._scale = np.ones(4, dtype=np.float32)
+        self._translate = np.zeros(4, dtype=np.float32)
+
+        s = ((1.0, 1.0, 1.0, 1.0) if scale is None else
+             as_vec4(scale, default=(1, 1, 1, 1)))
+        t = ((0.0, 0.0, 0.0, 0.0) if translate is None else
+             as_vec4(translate, default=(0, 0, 0, 0)))
+        self._set_st(s, t)
+
+    @arg_to_vec4
+    def map(self, coords):
+        """Map coordinates
+
+        Parameters
+        ----------
+        coords : array-like
+            Coordinates to map.
+
+        Returns
+        -------
+        coords : ndarray
+            Coordinates.
+        """
+        m = np.empty(coords.shape)
+        m[:, :3] = (coords[:, :3] * self.scale[np.newaxis, :3] +
+                    coords[:, 3:] * self.translate[np.newaxis, :3])
+        m[:, 3] = coords[:, 3]
+        return m
+
+    @arg_to_vec4
+    def imap(self, coords):
+        """Invert map coordinates
+
+        Parameters
+        ----------
+        coords : array-like
+            Coordinates to inverse map.
+
+        Returns
+        -------
+        coords : ndarray
+            Coordinates.
+        """
+        m = np.empty(coords.shape)
+        m[:, :3] = ((coords[:, :3] -
+                     coords[:, 3:] * self.translate[np.newaxis, :3]) /
+                    self.scale[np.newaxis, :3])
+        m[:, 3] = coords[:, 3]
+        return m
+
+    def shader_map(self):
+        if self._update_map:
+            self._shader_map['scale'] = self.scale
+            self._shader_map['translate'] = self.translate
+            self._update_map = False
+        return self._shader_map
+
+    def shader_imap(self):
+        if self._update_imap:
+            self._shader_imap['scale'] = self.scale
+            self._shader_imap['translate'] = self.translate
+            self._update_imap = False
+        return self._shader_imap
+
+    @property
+    def scale(self):
+        return self._scale.copy()
+
+    @scale.setter
+    def scale(self, s):
+        s = as_vec4(s, default=(1, 1, 1, 1))
+        self._set_st(scale=s)
+
+    @property
+    def translate(self):
+        return self._translate.copy()
+
+    @translate.setter
+    def translate(self, t):
+        t = as_vec4(t, default=(0, 0, 0, 0))
+        self._set_st(translate=t)
+
+    def _set_st(self, scale=None, translate=None):
+        update = False
+
+        if scale is not None and not np.all(scale == self._scale):
+            self._scale[:] = scale
+            update = True
+
+        if translate is not None and not np.all(translate == self._translate):
+            self._translate[:] = translate
+            update = True
+
+        if update:
+            self._update_map = True
+            self._update_imap = True
+            self.update()   # inform listeners there has been a change
+
+    def move(self, move):
+        """Change the translation of this transform by the amount given.
+
+        Parameters
+        ----------
+        move : array-like
+            The values to be added to the current translation of the transform.
+        """
+        move = as_vec4(move, default=(0, 0, 0, 0))
+        self.translate = self.translate + move
+
+    def zoom(self, zoom, center=(0, 0, 0), mapped=True):
+        """Update the transform such that its scale factor is changed, but
+        the specified center point is left unchanged.
+
+        Parameters
+        ----------
+        zoom : array-like
+            Values to multiply the transform's current scale
+            factors.
+        center : array-like
+            The center point around which the scaling will take place.
+        mapped : bool
+            Whether *center* is expressed in mapped coordinates (True) or
+            unmapped coordinates (False).
+        """
+        zoom = as_vec4(zoom, default=(1, 1, 1, 1))
+        center = as_vec4(center, default=(0, 0, 0, 0))
+        scale = self.scale * zoom
+        if mapped:
+            trans = center - (center - self.translate) * zoom
+        else:
+            trans = self.scale * (1 - zoom) * center + self.translate
+        self._set_st(scale=scale, translate=trans)
+
+    def as_affine(self):
+        m = AffineTransform()
+        m.scale(self.scale)
+        m.translate(self.translate)
+        return m
+
+    @classmethod
+    def from_mapping(cls, x0, x1):
+        """ Create an STTransform from the given mapping
+
+        See `set_mapping` for details.
+
+        Parameters
+        ----------
+        x0 : array-like
+            Start.
+        x1 : array-like
+            End.
+
+        Returns
+        -------
+        t : instance of STTransform
+            The transform.
+        """
+        t = cls()
+        t.set_mapping(x0, x1)
+        return t
+
+    def set_mapping(self, x0, x1):
+        """Configure this transform such that it maps points x0 => x1
+
+        Parameters
+        ----------
+        x0 : array-like, shape (2, 2) or (2, 3)
+            Start location.
+        x1 : array-like, shape (2, 2) or (2, 3)
+            End location.
+
+        Examples
+        --------
+        For example, if we wish to map the corners of a rectangle::
+
+            >>> p1 = [[0, 0], [200, 300]]
+
+        onto a unit cube::
+
+            >>> p2 = [[-1, -1], [1, 1]]
+
+        then we can generate the transform as follows::
+
+            >>> tr = STTransform()
+            >>> tr.set_mapping(p1, p2)
+            >>> assert tr.map(p1)[:,:2] == p2  # test
+
+        """
+        # if args are Rect, convert to array first
+        if isinstance(x0, Rect):
+            x0 = x0._transform_in()[:3]
+        if isinstance(x1, Rect):
+            x1 = x1._transform_in()[:3]
+        
+        x0 = np.asarray(x0)
+        x1 = np.asarray(x1)
+        if (x0.ndim != 2 or x0.shape[0] != 2 or x1.ndim != 2 or 
+                x1.shape[0] != 2):
+            raise TypeError("set_mapping requires array inputs of shape "
+                            "(2, N).")
+        denom = x0[1] - x0[0]
+        mask = denom == 0
+        denom[mask] = 1.0
+        s = (x1[1] - x1[0]) / denom
+        s[mask] = 1.0
+        s[x0[1] == x0[0]] = 1.0
+        t = x1[0] - s * x0[0]
+        s = as_vec4(s, default=(1, 1, 1, 1))
+        t = as_vec4(t, default=(0, 0, 0, 0))
+        self._set_st(scale=s, translate=t)
+
+    def __mul__(self, tr):
+        if isinstance(tr, STTransform):
+            s = self.scale * tr.scale
+            t = self.translate + (tr.translate * self.scale)
+            return STTransform(scale=s, translate=t)
+        elif isinstance(tr, AffineTransform):
+            return self.as_affine() * tr
+        else:
+            return super(STTransform, self).__mul__(tr)
+
+    def __rmul__(self, tr):
+        if isinstance(tr, AffineTransform):
+            return tr * self.as_affine()
+        return super(STTransform, self).__rmul__(tr)
+
+    def __repr__(self):
+        return ("<STTransform scale=%s translate=%s>"
+                % (self.scale, self.translate))
+
+
+class AffineTransform(BaseTransform):
+    """Affine transformation class
+
+    Parameters
+    ----------
+    matrix : array-like | None
+        4x4 array to use for the transform.
+    """
+    glsl_map = """
+        vec4 affine_transform_map(vec4 pos) {
+            return $matrix * pos;
+        }
+    """
+
+    glsl_imap = """
+        vec4 affine_transform_imap(vec4 pos) {
+            return $inv_matrix * pos;
+        }
+    """
+
+    Linear = True
+    Orthogonal = False
+    NonScaling = False
+    Isometric = False
+
+    def __init__(self, matrix=None):
+        super(AffineTransform, self).__init__()
+        if matrix is not None:
+            self.matrix = matrix
+        else:
+            self.reset()
+
+    @arg_to_vec4
+    def map(self, coords):
+        """Map coordinates
+
+        Parameters
+        ----------
+        coords : array-like
+            Coordinates to map.
+
+        Returns
+        -------
+        coords : ndarray
+            Coordinates.
+        """
+        # looks backwards, but both matrices are transposed.
+        return np.dot(coords, self.matrix)
+
+    @arg_to_vec4
+    def imap(self, coords):
+        """Inverse map coordinates
+
+        Parameters
+        ----------
+        coords : array-like
+            Coordinates to inverse map.
+
+        Returns
+        -------
+        coords : ndarray
+            Coordinates.
+        """
+        return np.dot(coords, self.inv_matrix)
+
+    def shader_map(self):
+        fn = super(AffineTransform, self).shader_map()
+        fn['matrix'] = self.matrix  # uniform mat4
+        return fn
+
+    def shader_imap(self):
+        fn = super(AffineTransform, self).shader_imap()
+        fn['inv_matrix'] = self.inv_matrix  # uniform mat4
+        return fn
+
+    @property
+    def matrix(self):
+        return self._matrix
+
+    @matrix.setter
+    def matrix(self, m):
+        self._matrix = m
+        self._inv_matrix = None
+        self.shader_map()
+        self.shader_imap()
+        self.update()
+
+    @property
+    def inv_matrix(self):
+        if self._inv_matrix is None:
+            self._inv_matrix = np.linalg.inv(self.matrix)
+        return self._inv_matrix
+
+    @arg_to_vec4
+    def translate(self, pos):
+        """
+        Translate the matrix
+
+        The translation is applied *after* the transformations already present
+        in the matrix.
+
+        Parameters
+        ----------
+        pos : arrayndarray
+            Position to translate by.
+        """
+        self.matrix = np.dot(self.matrix, transforms.translate(pos[0, :3]))
+
+    def scale(self, scale, center=None):
+        """
+        Scale the matrix about a given origin.
+
+        The scaling is applied *after* the transformations already present
+        in the matrix.
+
+        Parameters
+        ----------
+        scale : array-like
+            Scale factors along x, y and z axes.
+        center : array-like or None
+            The x, y and z coordinates to scale around. If None,
+            (0, 0, 0) will be used.
+        """
+        scale = transforms.scale(as_vec4(scale, default=(1, 1, 1, 1))[0, :3])
+        if center is not None:
+            center = as_vec4(center)[0, :3]
+            scale = np.dot(np.dot(transforms.translate(-center), scale),
+                           transforms.translate(center))
+        self.matrix = np.dot(self.matrix, scale)
+
+    def rotate(self, angle, axis):
+        """
+        Rotate the matrix by some angle about a given axis.
+
+        The rotation is applied *after* the transformations already present
+        in the matrix.
+
+        Parameters
+        ----------
+        angle : float
+            The angle of rotation, in degrees.
+        axis : array-like
+            The x, y and z coordinates of the axis vector to rotate around.
+        """
+        self.matrix = np.dot(self.matrix, transforms.rotate(angle, axis))
+
+    def set_mapping(self, points1, points2):
+        """ Set to a 3D transformation matrix that maps points1 onto points2.
+
+        Parameters
+        ----------
+        points1 : array-like, shape (4, 3)
+            Four starting 3D coordinates.
+        points2 : array-like, shape (4, 3)
+            Four ending 3D coordinates.
+        """
+        # note: need to transpose because util.functions uses opposite
+        # of standard linear algebra order.
+        self.matrix = transforms.affine_map(points1, points2).T
+
+    def set_ortho(self, l, r, b, t, n, f):
+        """Set ortho transform
+
+        Parameters
+        ----------
+        l : float
+            Left.
+        r : float
+            Right.
+        b : float
+            Bottom.
+        t : float
+            Top.
+        n : float
+            Near.
+        f : float
+            Far.
+        """
+        self.matrix = transforms.ortho(l, r, b, t, n, f)
+
+    def reset(self):
+        self.matrix = np.eye(4)
+
+    def __mul__(self, tr):
+        if (isinstance(tr, AffineTransform) and not
+                any(tr.matrix[:3, 3] != 0)):
+            # don't multiply if the perspective column is used
+            return AffineTransform(matrix=np.dot(tr.matrix, self.matrix))
+        else:
+            return tr.__rmul__(self)
+
+    def __repr__(self):
+        s = "%s(matrix=[" % self.__class__.__name__
+        indent = " "*len(s)
+        s += str(list(self.matrix[0])) + ",\n"
+        s += indent + str(list(self.matrix[1])) + ",\n"
+        s += indent + str(list(self.matrix[2])) + ",\n"
+        s += indent + str(list(self.matrix[3])) + "] at 0x%x)" % id(self)
+        return s
+
+
+#class SRTTransform(BaseTransform):
+#    """ Transform performing scale, rotate, and translate, in that order.
+#
+#    This transformation allows objects to be placed arbitrarily in a scene
+#    much the same way AffineTransform does. However, an incorrect order of
+#    operations in AffineTransform may result in shearing the object (if scale
+#    is applied after rotate) or in unpredictable translation (if scale/rotate
+#    is applied after translation). SRTTransform avoids these problems by
+#    enforcing the correct order of operations.
+#    """
+#    # TODO
+
+
+class PerspectiveTransform(AffineTransform):
+    """
+    Matrix transform that also implements perspective division.
+
+    Parameters
+    ----------
+    matrix : array-like | None
+        4x4 array to use for the transform.
+    """
+    def set_perspective(self, fov, aspect, near, far):
+        """Set the perspective
+
+        Parameters
+        ----------
+        fov : float
+            Field of view.
+        aspect : float
+            Aspect ratio.
+        near : float
+            Near location.
+        far : float
+            Far location.
+        """
+        self.matrix = transforms.perspective(fov, aspect, near, far)
+
+    def set_frustum(self, l, r, b, t, n, f):
+        """Set the frustum
+
+        Parameters
+        ----------
+        l : float
+            Left.
+        r : float
+            Right.
+        b : float
+            Bottom.
+        t : float
+            Top.
+        n : float
+            Near.
+        f : float
+            Far.
+        """
+        self.matrix = transforms.frustum(l, r, b, t, n, f)
diff --git a/vispy/visuals/transforms/nonlinear.py b/vispy/visuals/transforms/nonlinear.py
new file mode 100644
index 0000000..d2a3d84
--- /dev/null
+++ b/vispy/visuals/transforms/nonlinear.py
@@ -0,0 +1,402 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division
+
+import numpy as np
+
+from ._util import arg_to_array, arg_to_vec4, as_vec4
+from .base_transform import BaseTransform
+from ... import gloo
+
+
+class LogTransform(BaseTransform):
+    """ Transform perfoming logarithmic transformation on three axes.
+
+    Maps (x, y, z) => (log(base.x, x), log(base.y, y), log(base.z, z))
+
+    No transformation is applied for axes with base == 0.
+
+    If base < 0, then the inverse function is applied: x => base.x ** x
+
+    Parameters
+    ----------
+    base : array-like
+        Base for the X, Y, Z axes.
+    """
+
+    # TODO: Evaluate the performance costs of using conditionals.
+    # An alternative approach is to transpose the vector before
+    # log-transforming, and then transpose back afterward.
+    glsl_map = """
+        vec4 LogTransform_map(vec4 pos) {
+            if($base.x > 1.0)
+                pos.x = log(pos.x) / log($base.x);
+            else if($base.x < -1.0)
+                pos.x = pow(-$base.x, pos.x);
+
+            if($base.y > 1.0)
+                pos.y = log(pos.y) / log($base.y);
+            else if($base.y < -1.0)
+                pos.y = pow(-$base.y, pos.y);
+
+            if($base.z > 1.0)
+                pos.z = log(pos.z) / log($base.z);
+            else if($base.z < -1.0)
+                pos.z = pow(-$base.z, pos.z);
+            return pos;
+        }
+        """
+
+    glsl_imap = glsl_map
+
+    Linear = False
+    Orthogonal = True
+    NonScaling = False
+    Isometric = False
+
+    def __init__(self, base=None):
+        super(LogTransform, self).__init__()
+        self._base = np.zeros(3, dtype=np.float32)
+        self.base = (0.0, 0.0, 0.0) if base is None else base
+
+    @property
+    def base(self):
+        """
+        *base* is a tuple (x, y, z) containing the log base that should be
+        applied to each axis of the input vector. If any axis has a base <= 0,
+        then that axis is not affected.
+        """
+        return self._base.copy()
+
+    @base.setter
+    def base(self, s):
+        self._base[:len(s)] = s
+        self._base[len(s):] = 0.0
+
+    @arg_to_array
+    def map(self, coords, base=None):
+        ret = np.empty(coords.shape, coords.dtype)
+        if base is None:
+            base = self.base
+        for i in range(min(ret.shape[-1], 3)):
+            if base[i] > 1.0:
+                ret[..., i] = np.log(coords[..., i]) / np.log(base[i])
+            elif base[i] < -1.0:
+                ret[..., i] = -base[i] ** coords[..., i]
+            else:
+                ret[..., i] = coords[..., i]
+        return ret
+
+    @arg_to_array
+    def imap(self, coords):
+        return self.map(coords, -self.base)
+
+    def shader_map(self):
+        fn = super(LogTransform, self).shader_map()
+        fn['base'] = self.base  # uniform vec3
+        return fn
+
+    def shader_imap(self):
+        fn = super(LogTransform, self).shader_imap()
+        fn['base'] = -self.base  # uniform vec3
+        return fn
+
+    def __repr__(self):
+        return "<LogTransform base=%s>" % (self.base)
+
+
+class PolarTransform(BaseTransform):
+    """Polar transform
+
+    Maps (theta, r, z) to (x, y, z), where `x = r*cos(theta)`
+    and `y = r*sin(theta)`.
+    """
+    glsl_map = """
+        vec4 polar_transform_map(vec4 pos) {
+            return vec4(pos.y * cos(pos.x), pos.y * sin(pos.x), pos.z, 1);
+        }
+        """
+
+    glsl_imap = """
+        vec4 polar_transform_map(vec4 pos) {
+            // TODO: need some modulo math to handle larger theta values..?
+            float theta = atan(pos.y, pos.x);
+            float r = length(pos.xy);
+            return vec4(theta, r, pos.z, 1);
+        }
+        """
+
+    Linear = False
+    Orthogonal = False
+    NonScaling = False
+    Isometric = False
+
+    @arg_to_array
+    def map(self, coords):
+        ret = np.empty(coords.shape, coords.dtype)
+        ret[..., 0] = coords[..., 1] * np.cos(coords[..., 0])
+        ret[..., 1] = coords[..., 1] * np.sin(coords[..., 0])
+        for i in range(2, coords.shape[-1]):  # copy any further axes
+            ret[..., i] = coords[..., i]
+        return ret
+
+    @arg_to_array
+    def imap(self, coords):
+        ret = np.empty(coords.shape, coords.dtype)
+        ret[..., 0] = np.arctan2(coords[..., 0], coords[..., 1])
+        ret[..., 1] = (coords[..., 0]**2 + coords[..., 1]**2) ** 0.5
+        for i in range(2, coords.shape[-1]):  # copy any further axes
+            ret[..., i] = coords[..., i]
+        return ret
+
+
+#class BilinearTransform(BaseTransform):
+#    # TODO
+#    pass
+
+
+#class WarpTransform(BaseTransform):
+#    """ Multiple bilinear transforms in a grid arrangement.
+#    """
+#    # TODO
+
+
+class MagnifyTransform(BaseTransform):
+    """ Magnifying lens transform. 
+
+    This transform causes a circular region to appear with larger scale around
+    its center point. 
+    
+    Parameters
+    ----------
+    mag : float
+        Magnification factor. Objects around the transform's center point will
+        appear scaled by this amount relative to objects outside the circle.
+    radii : (float, float)
+        Inner and outer radii of the "lens". Objects inside the inner radius
+        appear scaled, whereas objects outside the outer radius are unscaled,
+        and the scale factor transitions smoothly between the two radii.
+    center: (float, float)
+        The center (x, y) point of the "lens".
+        
+    Notes
+    -----
+    
+    This transform works by segmenting its input coordinates into three
+    regions--inner, outer, and transition. Coordinates in the inner region are
+    multiplied by a constant scale factor around the center point, and 
+    coordinates in the transition region are scaled by a factor that 
+    transitions smoothly from the inner radius to the outer radius. 
+    
+    Smooth functions that are appropriate for the transition region also tend 
+    to be difficult to invert analytically, so this transform instead samples
+    the function numerically to allow trivial inversion. In OpenGL, the 
+    sampling is implemented as a texture holding a lookup table.
+    """
+    glsl_map = """
+        vec4 mag_transform(vec4 pos) {
+            vec2 d = vec2(pos.x - $center.x, pos.y - $center.y);
+            float dist = length(d);
+            if (dist == 0 || dist > $radii.y || ($mag < 1.01 && $mag > 0.99)) {
+                return pos;
+            }
+            vec2 dir = d / dist;
+            
+            if( dist < $radii.x ) {
+                dist = dist * $mag;
+            }
+            else {
+                
+                float r1 = $radii.x;
+                float r2 = $radii.y;
+                float x = (dist - r1) / (r2 - r1);
+                float s = texture2D($trans, vec2(0, x)).r * $trans_max;
+                
+                dist = s;
+            }
+
+            d = $center + dir * dist;
+            return vec4(d, pos.z, pos.w);
+        }"""
+    
+    glsl_imap = glsl_map
+    
+    Linear = False
+    
+    _trans_resolution = 1000
+    
+    def __init__(self, mag=3, radii=(7, 10), center=(0, 0)):
+        self._center = center
+        self._mag = mag
+        self._radii = radii
+        self._trans = None
+        res = self._trans_resolution
+        self._trans_tex = (gloo.Texture2D((res, 1, 1), interpolation='linear'), 
+                           gloo.Texture2D((res, 1, 1), interpolation='linear'))
+        self._trans_tex_max = None
+        super(MagnifyTransform, self).__init__()
+        
+    @property
+    def center(self):
+        """ The (x, y) center point of the transform.
+        """
+        return self._center
+    
+    @center.setter
+    def center(self, center):
+        if np.allclose(self._center, center):
+            return
+        self._center = center
+        self.shader_map()
+        self.shader_imap()
+
+    @property
+    def mag(self):
+        """ The scale factor used in the central region of the transform.
+        """
+        return self._mag
+    
+    @mag.setter
+    def mag(self, mag):
+        if self._mag == mag:
+            return
+        self._mag = mag
+        self._trans = None
+        self.shader_map()
+        self.shader_imap()
+
+    @property
+    def radii(self):
+        """ The inner and outer radii of the circular area bounding the 
+        transform.
+        """
+        return self._radii
+    
+    @radii.setter
+    def radii(self, radii):
+        if np.allclose(self._radii, radii):
+            return
+        self._radii = radii
+        self._trans = None
+        self.shader_map()
+        self.shader_imap()
+
+    def shader_map(self):
+        fn = super(MagnifyTransform, self).shader_map()
+        fn['center'] = self._center  # uniform vec2
+        fn['mag'] = self._mag
+        fn['radii'] = (self._radii[0] / self._mag, self._radii[1])
+        self._get_transition()  # make sure transition texture is up to date
+        fn['trans'] = self._trans_tex[0]
+        fn['trans_max'] = self._trans_tex_max[0]
+        return fn
+
+    def shader_imap(self):
+        fn = super(MagnifyTransform, self).shader_imap()
+        fn['center'] = self._center  # uniform vec2
+        fn['mag'] = 1. / self._mag
+        fn['radii'] = self._radii
+        self._get_transition()  # make sure transition texture is up to date
+        fn['trans'] = self._trans_tex[1]
+        fn['trans_max'] = self._trans_tex_max[1]
+        return fn
+
+    @arg_to_vec4
+    def map(self, x, _inverse=False):
+        c = as_vec4(self.center)[0]
+        m = self.mag
+        r1, r2 = self.radii
+        
+        #c = np.array(c).reshape(1,2)
+        xm = np.empty(x.shape, dtype=x.dtype)
+        
+        dx = (x - c)
+        dist = (((dx**2).sum(axis=-1)) ** 0.5)[..., np.newaxis]
+        dist[np.isnan(dist)] = 0
+        unit = dx / dist
+        
+        # magnified center region
+        if _inverse:
+            inner = (dist < r1)[:, 0]
+            s = dist / m
+        else:
+            inner = (dist < (r1 / m))[:, 0]
+            s = dist * m
+        xm[inner] = c + unit[inner] * s[inner]
+        
+        # unmagnified outer region
+        outer = (dist > r2)[:, 0]  
+        xm[outer] = x[outer]
+        
+        # smooth transition region, interpolated from trans
+        trans = ~(inner | outer)
+
+        # look up scale factor from trans
+        temp, itemp = self._get_transition()
+        if _inverse:
+            tind = (dist[trans] - r1) * len(itemp) / (r2 - r1)
+            temp = itemp
+        else:
+            tind = (dist[trans] - (r1/m)) * len(temp) / (r2 - (r1/m))
+        tind = np.clip(tind, 0, temp.shape[0]-1)
+        s = temp[tind.astype(int)]
+        
+        xm[trans] = c + unit[trans] * s
+        return xm
+
+    def imap(self, coords):
+        return self.map(coords, _inverse=True)
+
+    def _get_transition(self):
+        # Generate forward/reverse transition templates.
+        # We would prefer to express this with an invertible function, but that
+        # turns out to be tricky. The templates make any function invertible.
+        
+        if self._trans is None:
+            m, r1, r2 = self.mag, self.radii[0], self.radii[1]
+            res = self._trans_resolution
+            
+            xi = np.linspace(r1, r2, res)
+            t = 0.5 * (1 + np.cos((xi - r2) * np.pi / (r2 - r1)))
+            yi = (xi * t + xi * (1-t) / m).astype(np.float32)
+            x = np.linspace(r1 / m, r2, res)
+            y = np.interp(x, yi, xi).astype(np.float32)
+            
+            self._trans = (y, yi)
+            # scale to 0.0-1.0 to prevent clipping (is this necessary?)
+            mx = y.max(), yi.max()
+            self._trans_tex_max = mx
+            self._trans_tex[0].set_data((y/mx[0])[:, np.newaxis, np.newaxis])
+            self._trans_tex[1].set_data((yi/mx[1])[:, np.newaxis, np.newaxis])
+            
+        return self._trans
+
+
+class Magnify1DTransform(MagnifyTransform):
+    """ A 1-dimensional analog of MagnifyTransform. This transform expands 
+    its input along the x-axis, around a center x value.
+    """
+    glsl_map = """
+        vec4 mag_transform(vec4 pos) {
+            float dist = pos.x - $center.x;
+            if (dist == 0 || abs(dist) > $radii.y || $mag == 1) {
+                return pos;
+            }
+            float dir = dist / abs(dist);
+            
+            if( abs(dist) < $radii.x ) {
+                dist = dist * $mag;
+            }
+            else {
+                float r1 = $radii.x;
+                float r2 = $radii.y;
+                float x = (abs(dist) - r1) / (r2 - r1);
+                dist = dir * texture2D($trans, vec2(0, x)).r * $trans_max;
+            }
+
+            return vec4($center.x + dist, pos.y, pos.z, pos.w);
+        }"""
+    
+    glsl_imap = glsl_map
diff --git a/vispy/scene/transforms/tests/test_transforms.py b/vispy/visuals/transforms/tests/test_transforms.py
similarity index 90%
rename from vispy/scene/transforms/tests/test_transforms.py
rename to vispy/visuals/transforms/tests/test_transforms.py
index 4c2629a..4ae8138 100644
--- a/vispy/scene/transforms/tests/test_transforms.py
+++ b/vispy/visuals/transforms/tests/test_transforms.py
@@ -1,15 +1,17 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014, Vispy Development Team.
+# Copyright (c) 2015, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
 
-import vispy.scene.transforms as tr
+import vispy.visuals.transforms as tr
 from vispy.geometry import Rect
+from vispy.testing import run_tests_if_main
 
 NT = tr.NullTransform
 ST = tr.STTransform
 AT = tr.AffineTransform
+RT = tr.PerspectiveTransform
 PT = tr.PolarTransform
 LT = tr.LogTransform
 CT = tr.ChainTransform
@@ -161,6 +163,21 @@ def test_map_rect():
     assert r1 == Rect((-6, 24), (26, 38))
 
 
+def test_st_transform():
+    # Check that STTransform maps exactly like AffineTransform
+    pts = np.random.normal(size=(10, 4))
+    
+    scale = (1, 7.5, -4e-8)
+    translate = (1e6, 0.2, 0)
+    st = tr.STTransform(scale=scale, translate=translate)
+    at = tr.AffineTransform()
+    at.scale(scale)
+    at.translate(translate)
+    
+    assert np.allclose(st.map(pts), at.map(pts))
+    assert np.allclose(st.inverse.map(pts), at.inverse.map(pts))    
+    
+
 def test_st_mapping():
     p1 = [[5., 7.], [23., 8.]]
     p2 = [[-1.3, -1.4], [1.1, 1.2]]
@@ -208,6 +225,7 @@ def test_inverse():
         NT(),
         ST(scale=(1e-4, 2e5), translate=(10, -6e9)),
         AT(m),
+        RT(m),
     ]
 
     np.random.seed(0)
@@ -225,9 +243,4 @@ def test_inverse():
     #assert np.allclose(abs_pos, tr.inverse.map(tr.map(abs_pos))[:,:3])
 
 
-if __name__ == '__main__':
-    for key in [key for key in globals()]:
-        if key.startswith('test_'):
-            func = globals()[key]
-            print('running', func.__name__)
-            func()
+run_tests_if_main()
diff --git a/vispy/visuals/transforms/transform_system.py b/vispy/visuals/transforms/transform_system.py
new file mode 100644
index 0000000..3e449b2
--- /dev/null
+++ b/vispy/visuals/transforms/transform_system.py
@@ -0,0 +1,237 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division
+
+from .linear import NullTransform, STTransform
+from ._util import TransformCache
+
+
+class TransformSystem(object):
+    """ TransformSystem encapsulates information about the coordinate
+    systems needed to draw a Visual.
+
+    Visual rendering operates in four coordinate systems:
+
+    * **Visual** - arbitrary local coordinate frame of the visual. Vertex
+      buffers used by the visual are usually specified in this coordinate
+      system.
+
+    * **Document** - This coordinate system has units of _logical_ pixels, and
+      should usually represent the pixel coordinates of the canvas being drawn
+      to. Visuals use this coordinate system to make measurements for font
+      size, line width, and in general anything that is specified in physical
+      units (px, pt, mm, in, etc.). Note that, by convention, _logical_ pixels
+      are not necessarily the same size as the _physical_ pixels in the
+      framebuffer that is being rendered to.
+
+    * **Buffer** - The buffer coordinate system has units of _physical_ pixels,
+      and should usually represent the coordinates of the current framebuffer
+      being rendered to. Visuals use this coordinate system primarily for
+      antialiasing calculations. In most cases, this will have the same scale
+      as the document coordinate system because the active framebuffer is the
+      back buffer of the canvas, and the canvas will have _logical_ and
+      _physical_ pixels of the same size. The scale may be different in the
+      case of high-resolution displays, or when rendering to an off-screen
+      framebuffer with different scaling or boundaries than the canvas.
+
+    * **Render** - This coordinate system is the obligatory system for
+      vertexes returned by a vertex shader. It has coordinates (-1, -1) to
+      (1, 1) across the current glViewport. In OpenGL terminology, this is
+      called normalized device coordinates.
+
+    Parameters
+    ----------
+
+    canvas : Canvas
+        The canvas being drawn to.
+    dpi : float
+        The dot-per-inch resolution of the document coordinate system. By
+        default this is set to the resolution of the canvas.
+
+    Notes
+    -----
+
+    By default, TransformSystems are configured such that the document
+    coordinate system matches the logical pixels of the canvas,
+
+    Examples
+    --------
+    Use by Visuals
+    ~~~~~~~~~~~~~~
+
+    1. To convert local vertex coordinates to normalized device coordinates in
+    the vertex shader, we first need a vertex shader that supports configurable
+    transformations::
+
+        vec4 a_position;
+        void main() {
+            gl_Position = $transform(a_position);
+        }
+
+    Next, we supply the complete chain of transforms when drawing the visual:
+
+        def draw(tr_sys):
+            tr = tr_sys.get_full_transform()
+            self.program['transform'] = tr.shader_map()
+            self.program['a_position'] = self.vertex_buffer
+            self.program.draw('triangles')
+
+    2. Draw a line whose width is given in mm. To start, we need normal vectors
+    for each vertex, which tell us the direction the vertex should move in
+    order to set the line width::
+
+        vec4 a_position;
+        vec4 a_normal;
+        float u_line_width;
+        float u_dpi;
+        void main() {
+            // map vertex position and normal vector to the document cs
+            vec4 doc_pos = $visual_to_doc(a_position);
+            vec4 doc_normal = $visual_to_doc(a_position + a_normal) - doc_pos;
+
+            // Use DPI to convert mm line width to logical pixels
+            float px_width = (u_line_width / 25.4) * dpi;
+
+            // expand by line width
+            doc_pos += normalize(doc_normal) * px_width;
+
+            // finally, map the remainder of the way to normalized device
+            // coordinates.
+            gl_Position = $doc_to_render(a_position);
+        }
+
+    In this case, we need to access
+    the transforms independently, so ``get_full_transform()`` is not useful
+    here::
+
+        def draw(tr_sys):
+            # Send two parts of the full transform separately
+            self.program['visual_to_doc'] = tr_sys.visual_to_doc.shader_map()
+            doc_to_render = (tr_sys.framebuffer_to_render *
+                             tr_sys.document_to_framebuffer)
+            self.program['visual_to_doc'] = doc_to_render.shader_map()
+
+            self.program['u_line_width'] = self.line_width
+            self.program['u_dpi'] = tr_sys.dpi
+            self.program['a_position'] = self.vertex_buffer
+            self.program['a_normal'] = self.normal_buffer
+            self.program.draw('triangles')
+
+    3. Draw a triangle with antialiasing at the edge.
+
+    4. Using inverse transforms in the fragment shader
+
+    Creating TransformSystem instances
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    1. Basic example, including checks for canvas resolution
+
+    2. How to handle off-screen framebuffers
+
+    """
+
+    def __init__(self, canvas, dpi=None):
+        self._canvas = canvas
+        self._cache = TransformCache()
+        if dpi is None:
+            dpi = canvas.dpi
+        self._dpi = dpi
+
+        # Null by default; visuals draw directly to the document coordinate
+        # system.
+        self._visual_to_document = NullTransform()
+        self._document_to_framebuffer = STTransform()
+        self._framebuffer_to_render = STTransform()
+
+        self.auto_configure()
+
+    def auto_configure(self):
+        """Automatically configure the TransformSystem:
+
+        * document_to_framebuffer maps from the Canvas logical pixel
+          coordinate system to the framebuffer coordinate system, assuming
+          physical pixels of the same size. The y-axis is inverted in this
+          transform.
+        * framebuffer_to_render maps from the framebuffer coordinate system to
+          normalized device coordinates (-1 to 1).
+        """
+        # By default, this should invert the y axis -- no difference between
+        # the scale of logical and physical pixels.
+        canvas = self._canvas
+        map_from = [(0, 0), canvas.size]
+        map_to = [(0, canvas.size[1]), (canvas.size[0], 0)]
+        self._document_to_framebuffer.set_mapping(map_from, map_to)
+
+        # Automatically configure buffer coordinate system to match the canvas
+        map_from = [(0, 0), canvas.size]
+        map_to = [(-1, -1), (1, 1)]
+        self._framebuffer_to_render.set_mapping(map_from, map_to)
+
+    @property
+    def canvas(self):
+        """ The Canvas being drawn to.
+        """
+        return self._canvas
+
+    @property
+    def dpi(self):
+        """ Physical resolution of the document coordinate system (dots per
+        inch).
+        """
+        return self._dpi
+
+    @dpi.setter
+    def dpi(self, dpi):
+        assert dpi > 0
+        self._dpi = dpi
+
+    @property
+    def visual_to_document(self):
+        """ Transform mapping from visual local coordinate frame to document
+        coordinate frame.
+        """
+        return self._visual_to_document
+
+    @visual_to_document.setter
+    def visual_to_document(self, tr):
+        if self._visual_to_document is not tr:
+            self._visual_to_document = tr
+
+    @property
+    def document_to_framebuffer(self):
+        """ Transform mapping from document coordinate frame to the framebuffer
+        (physical pixel) coordinate frame.
+        """
+        return self._document_to_framebuffer
+
+    @document_to_framebuffer.setter
+    def document_to_framebuffer(self, tr):
+        if self._document_to_framebuffer is not tr:
+            self._document_to_framebuffer = tr
+
+    @property
+    def framebuffer_to_render(self):
+        """ Transform mapping from pixel coordinate frame to rendering
+        coordinate frame.
+        """
+        return self._framebuffer_to_render
+
+    @framebuffer_to_render.setter
+    def framebuffer_to_render(self, tr):
+        if self._framebuffer_to_render is not tr:
+            self._framebuffer_to_render = tr
+
+    def get_full_transform(self):
+        """ Convenience method that returns the composition of all three
+        transforms::
+
+            framebuffer_to_render * document_to_framebuffer * visual_to_document
+
+        This is used for visuals that do not require physical measurements
+        or antialiasing.
+        """  # noqa
+        return self._cache.get([self.framebuffer_to_render,
+                                self.document_to_framebuffer,
+                                self.visual_to_document])
diff --git a/vispy/visuals/tube.py b/vispy/visuals/tube.py
new file mode 100644
index 0000000..0791f1b
--- /dev/null
+++ b/vispy/visuals/tube.py
@@ -0,0 +1,170 @@
+from __future__ import division
+
+from .mesh import MeshVisual
+import numpy as np
+from numpy.linalg import norm
+from ..util.transforms import rotate
+from ..color import ColorArray
+
+
+class TubeVisual(MeshVisual):
+    """Displays a tube around a piecewise-linear path.
+
+    The tube mesh is corrected following its Frenet curvature and
+    torsion such that it varies smoothly along the curve, including if
+    the tube is closed.
+
+    Parameters
+    ----------
+    points : ndarray
+        An array of (x, y, z) points describing the path along which the
+        tube will be extruded.
+    radius : float
+        The radius of the tube. Defaults to 1.0.
+    closed : bool
+        Whether the tube should be closed, joining the last point to the
+        first. Defaults to False.
+    color : Color | ColorArray
+        The color(s) to use when drawing the tube. The same color is
+        applied to each vertex of the mesh surrounding each point of
+        the line. If the input is a ColorArray, the argument will be
+        cycled; for instance if 'red' is passed then the entire tube
+        will be red, or if ['green', 'blue'] is passed then the points
+        will alternate between these colours. Defaults to 'purple'.
+    tube_points : int
+        The number of points in the circle-approximating polygon of the
+        tube's cross section. Defaults to 8.
+    shading : str | None
+        Same as for the `MeshVisual` class. Defaults to 'smooth'.
+    vertex_colors: ndarray | None
+        Same as for the `MeshVisual` class.
+    face_colors: ndarray | None
+        Same as for the `MeshVisual` class.
+    mode : str
+        Same as for the `MeshVisual` class. Defaults to 'triangles'.
+
+    """
+    def __init__(self, points, radius=1.0,
+                 closed=False,
+                 color='purple',
+                 tube_points=8,
+                 shading='smooth',
+                 vertex_colors=None,
+                 face_colors=None,
+                 mode='triangles'):
+
+        points = np.array(points)
+
+        tangents, normals, binormals = _frenet_frames(points, closed)
+
+        segments = len(points) - 1
+
+        # get the positions of each vertex
+        grid = np.zeros((len(points), tube_points, 3))
+        for i in range(len(points)):
+            pos = points[i]
+            normal = normals[i]
+            binormal = binormals[i]
+
+            # Add a vertex for each point on the circle
+            v = np.arange(tube_points,
+                          dtype=np.float) / tube_points * 2 * np.pi
+            cx = -1. * radius * np.cos(v)
+            cy = radius * np.sin(v)
+            grid[i] = (pos + cx[:, np.newaxis]*normal +
+                       cy[:, np.newaxis]*binormal)
+
+        # construct the mesh
+        indices = []
+        for i in range(segments):
+            for j in range(tube_points):
+                ip = (i+1) % segments if closed else i+1
+                jp = (j+1) % tube_points
+
+                index_a = i*tube_points + j
+                index_b = ip*tube_points + j
+                index_c = ip*tube_points + jp
+                index_d = i*tube_points + jp
+
+                indices.append([index_a, index_b, index_d])
+                indices.append([index_b, index_c, index_d])
+
+        vertices = grid.reshape(grid.shape[0]*grid.shape[1], 3)
+
+        color = ColorArray(color)
+        if vertex_colors is None:
+            point_colors = np.resize(color.rgba,
+                                     (len(points), 4))
+            vertex_colors = np.repeat(point_colors, tube_points, axis=0)
+
+        indices = np.array(indices, dtype=np.uint32)
+
+        MeshVisual.__init__(self, vertices, indices,
+                            vertex_colors=vertex_colors,
+                            face_colors=face_colors,
+                            shading=shading,
+                            mode=mode)
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        MeshVisual.draw(self, transforms)
+
+
+def _frenet_frames(points, closed):
+    '''Calculates and returns the tangents, normals and binormals for
+    the tube.'''
+    tangents = np.zeros((len(points), 3))
+    normals = np.zeros((len(points), 3))
+
+    epsilon = 0.0001
+
+    # Compute tangent vectors for each segment
+    tangents = np.roll(points, -1, axis=0) - np.roll(points, 1, axis=0)
+    if not closed:
+        tangents[0] = points[1] - points[0]
+        tangents[-1] = points[-1] - points[-2]
+    mags = np.sqrt(np.sum(tangents * tangents, axis=1))
+    tangents /= mags[:, np.newaxis]
+
+    # Get initial normal and binormal
+    t = np.abs(tangents[0])
+
+    smallest = np.argmin(t)
+    normal = np.zeros(3)
+    normal[smallest] = 1.
+
+    vec = np.cross(tangents[0], normal)
+
+    normals[0] = np.cross(tangents[0], vec)
+
+    # Compute normal and binormal vectors along the path
+    for i in range(1, len(points)):
+        normals[i] = normals[i-1]
+
+        vec = np.cross(tangents[i-1], tangents[i])
+        if norm(vec) > epsilon:
+            vec /= norm(vec)
+            theta = np.arccos(np.clip(tangents[i-1].dot(tangents[i]), -1, 1))
+            normals[i] = rotate(-np.degrees(theta),
+                                vec)[:3, :3].dot(normals[i])
+
+    if closed:
+        theta = np.arccos(np.clip(normals[0].dot(normals[-1]), -1, 1))
+        theta /= len(points) - 1
+
+        if tangents[0].dot(np.cross(normals[0], normals[-1])) > 0:
+            theta *= -1.
+
+        for i in range(1, len(points)):
+            normals[i] = rotate(-np.degrees(theta*i),
+                                tangents[i])[:3, :3].dot(normals[i])
+
+    binormals = np.cross(tangents, normals)
+
+    return tangents, normals, binormals
diff --git a/vispy/visuals/visual.py b/vispy/visuals/visual.py
new file mode 100644
index 0000000..1652667
--- /dev/null
+++ b/vispy/visuals/visual.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+from __future__ import division
+
+from ..util.event import EmitterGroup, Event
+from .shaders import StatementList
+from .. import gloo
+
+"""
+API Issues to work out:
+
+  * Need Visual.bounds() as described here:
+    https://github.com/vispy/vispy/issues/141
+
+"""
+
+
+class Visual(object):
+    """
+    Abstract class representing a drawable object.
+
+    At a minimum, Visual subclasses should extend the draw() method.
+
+    Events:
+
+        update : Event
+            Emitted when the visual has changed and needs to be redrawn.
+        bounds_change : Event
+            Emitted when the bounds of the visual have changed.
+
+    Notes
+    -----
+    When used in the scenegraph, all Visual classes are mixed with
+    `vispy.scene.Node` in order to implement the methods, attributes and
+    capabilities required for their usage within it.
+    """
+
+    def __init__(self):
+        self._visible = True
+        self.events = EmitterGroup(source=self,
+                                   auto_connect=True,
+                                   update=Event,
+                                   bounds_change=Event
+                                   )
+        self._gl_state = {'preset': None}
+        self._filters = set()
+        self._hooks = {}
+
+    def set_gl_state(self, preset=None, **kwargs):
+        """Define the set of GL state parameters to use when drawing
+
+        Parameters
+        ----------
+        preset : str
+            Preset to use.
+        **kwargs : dict
+            Keyword argments to use.
+        """
+        self._gl_state = kwargs
+        self._gl_state['preset'] = preset
+
+    def update_gl_state(self, *args, **kwargs):
+        """Modify the set of GL state parameters to use when drawing
+
+        Parameters
+        ----------
+        *args : tuple
+            Arguments.
+        **kwargs : dict
+            Keyword argments.
+        """
+        if len(args) == 1:
+            self._gl_state['preset'] = args[0]
+        elif len(args) != 0:
+            raise TypeError("Only one positional argument allowed.")
+        self._gl_state.update(kwargs)
+
+    def _update(self):
+        """
+        This method is called internally whenever the Visual needs to be 
+        redrawn. By default, it emits the update event.
+        """
+        self.events.update()
+
+    def draw(self, transforms):
+        """Draw this visual now.
+
+        The default implementation calls gloo.set_state().
+        
+        This function is called automatically when the visual needs to be drawn
+        as part of a scenegraph, or when calling 
+        ``SceneCanvas.draw_visual(...)``. It is uncommon to call this method 
+        manually.
+        
+        The *transforms* argument is a TransformSystem instance that provides 
+        access to transforms that the visual
+        may use to determine its relationship to the document coordinate
+        system (which provides physical measurements) and the framebuffer
+        coordinate system (which is necessary for antialiasing calculations). 
+        
+        Vertex transformation can be done either on the CPU using 
+        Transform.map(), or on the GPU using the GLSL functions generated by 
+        Transform.shader_map().
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        gloo.set_state(**self._gl_state)
+
+    def bounds(self, mode, axis):
+        """ Return the (min, max) bounding values describing the location of
+        this node in its local coordinate system.
+
+        Parameters
+        ----------
+        mode : str
+            Describes the type of boundary requested. Can be "visual", "data",
+            or "mouse".
+        axis : 0, 1, 2
+            The axis along which to measure the bounding values, in
+            x-y-z order.
+        
+        Returns
+        -------
+        None or (min, max) tuple. 
+        
+        Notes
+        -----
+        This is used primarily to allow automatic ViewBox zoom/pan.
+        By default, this method returns None which indicates the object should 
+        be ignored for automatic zooming along *axis*.
+        
+        A scenegraph may also use this information to cull visuals from the
+        display list.
+        
+        """
+        return None
+
+    def update(self):
+        """
+        Emit an event to inform listeners that this Visual needs to be redrawn.
+        """
+        self.events.update()
+
+    def _get_hook(self, shader, name):
+        """Return a FunctionChain that Filters may use to modify the program.
+
+        *shader* should be "frag" or "vert"
+        *name* should be "pre" or "post"
+        """
+        assert name in ('pre', 'post')
+        key = (shader, name)
+        if key in self._hooks:
+            return self._hooks[key]
+
+        prog = getattr(self, '_program', None)
+        if prog is None:
+            raise NotImplementedError("%s shader does not implement hook '%s'"
+                                      % key)
+        hook = StatementList()
+        if shader == 'vert':
+            prog.vert[name] = hook
+        elif shader == 'frag':
+            prog.frag[name] = hook
+        self._hooks[key] = hook
+        return hook
+
+    def attach(self, filt):
+        """Attach a Filter to this visual
+
+        Each filter modifies the appearance or behavior of the visual.
+
+        Parameters
+        ----------
+        filt : object
+            The filter to attach.
+        """
+        filt._attach(self)
+        self._filters.add(filt)
+
+    def detach(self, filt):
+        """Detach a filter
+
+        Parameters
+        ----------
+        filt : object
+            The filter to detach.
+        """
+        self._filters.remove(filt)
+        filt._detach(self)
diff --git a/vispy/visuals/volume.py b/vispy/visuals/volume.py
new file mode 100644
index 0000000..46c33c6
--- /dev/null
+++ b/vispy/visuals/volume.py
@@ -0,0 +1,676 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+About this technique
+--------------------
+
+In Python, we define the six faces of a cuboid to draw, as well as
+texture cooridnates corresponding with the vertices of the cuboid. 
+The back faces of the cuboid are drawn (and front faces are culled)
+because only the back faces are visible when the camera is inside the 
+volume.
+
+In the vertex shader, we intersect the view ray with the near and far 
+clipping planes. In the fragment shader, we use these two points to
+compute the ray direction and then compute the position of the front
+cuboid surface (or near clipping plane) along the view ray.
+
+Next we calculate the number of steps to walk from the front surface
+to the back surface and iterate over these positions in a for-loop.
+At each iteration, the fragment color or other voxel information is 
+updated depending on the selected rendering method.
+
+It is important for the texture interpolation is 'linear', since with
+nearest the result look very ugly. The wrapping should be clamp_to_edge
+to avoid artifacts when the ray takes a small step outside the volume.
+
+The ray direction is established by mapping the vertex to the document
+coordinate frame, adjusting z to +/-1, and mapping the coordinate back.
+The ray is expressed in coordinates local to the volume (i.e. texture
+coordinates).
+
+"""
+
+from ..gloo import Texture3D, TextureEmulated3D, VertexBuffer, IndexBuffer
+from . import Visual
+from .shaders import Function, ModularProgram
+from ..color import get_colormap
+
+import numpy as np
+
+# todo: implement more render methods (port from visvis)
+# todo: allow anisotropic data
+# todo: what to do about lighting? ambi/diffuse/spec/shinynes on each visual?
+
+# Vertex shader
+VERT_SHADER = """
+attribute vec3 a_position;
+attribute vec3 a_texcoord;
+uniform vec3 u_shape;
+
+varying vec3 v_texcoord;
+varying vec3 v_position;
+varying vec4 v_nearpos;
+varying vec4 v_farpos;
+
+void main() {
+    v_texcoord = a_texcoord;
+    v_position = a_position;
+    
+    // Project local vertex coordinate to camera position. Then do a step
+    // backward (in cam coords) and project back. Voila, we get our ray vector.
+    vec4 pos_in_cam = $viewtransformf(vec4(v_position, 1));
+
+    // intersection of ray and near clipping plane (z = -1 in clip coords)
+    pos_in_cam.z = -pos_in_cam.w;
+    v_nearpos = $viewtransformi(pos_in_cam);
+    
+    // intersection of ray and far clipping plane (z = +1 in clip coords)
+    pos_in_cam.z = pos_in_cam.w;
+    v_farpos = $viewtransformi(pos_in_cam);
+    
+    gl_Position = $transform(vec4(v_position, 1.0));
+}
+"""  # noqa
+
+# Fragment shader
+FRAG_SHADER = """
+// uniforms
+uniform $sampler_type u_volumetex;
+uniform vec3 u_shape;
+uniform float u_threshold;
+uniform float u_relative_step_size;
+
+//varyings
+varying vec3 v_texcoord;
+varying vec3 v_position;
+varying vec4 v_nearpos;
+varying vec4 v_farpos;
+
+// uniforms for lighting. Hard coded until we figure out how to do lights
+const vec4 u_ambient = vec4(0.2, 0.4, 0.2, 1.0);
+const vec4 u_diffuse = vec4(0.8, 0.2, 0.2, 1.0);
+const vec4 u_specular = vec4(1.0, 1.0, 1.0, 1.0);
+const float u_shininess = 40.0;
+
+//varying vec3 lightDirs[1];
+
+// global holding view direction in local coordinates
+vec3 view_ray;
+
+vec4 calculateColor(vec4, vec3, vec3);
+float rand(vec2 co);
+
+void main() {{
+    vec3 farpos = v_farpos.xyz / v_farpos.w;
+    vec3 nearpos = v_nearpos.xyz / v_nearpos.w;
+    
+    // Calculate unit vector pointing in the view direction through this 
+    // fragment.
+    view_ray = normalize(farpos.xyz - nearpos.xyz);
+    
+    // Compute the distance to the front surface or near clipping plane
+    float distance = dot(nearpos-v_position, view_ray);
+    distance = max(distance, min((-0.5 - v_position.x) / view_ray.x, 
+                            (u_shape.x - 0.5 - v_position.x) / view_ray.x));
+    distance = max(distance, min((-0.5 - v_position.y) / view_ray.y, 
+                            (u_shape.y - 0.5 - v_position.y) / view_ray.y));
+    distance = max(distance, min((-0.5 - v_position.z) / view_ray.z, 
+                            (u_shape.z - 0.5 - v_position.z) / view_ray.z));
+    
+    // Now we have the starting position on the front surface
+    vec3 front = v_position + view_ray * distance;
+    
+    // Decide how many steps to take
+    int nsteps = int(-distance / u_relative_step_size + 0.5);
+    if( nsteps < 1 )
+        discard;
+        
+    // Get starting location and step vector in texture coordinates
+    vec3 step = ((v_position - front) / u_shape) / nsteps;
+    vec3 start_loc = front / u_shape;
+    
+    // For testing: show the number of steps. This helps to establish
+    // whether the rays are correctly oriented
+    //gl_FragColor = vec4(0.0, nsteps / 3.0 / u_shape.x, 1.0, 1.0);
+    //return;
+    
+    {before_loop}
+    
+    // This outer loop seems necessary on some systems for large
+    // datasets. Ugly, but it works ...
+    vec3 loc = start_loc;
+    int iter = 0;
+    while (iter < nsteps) {{
+        for (iter=iter; iter<nsteps; iter++)
+        {{
+            // Get sample color
+            vec4 color = $sample(u_volumetex, loc);
+            float val = color.g;
+            
+            {in_loop}
+            
+            // Advance location deeper into the volume
+            loc += step;
+        }}
+    }}
+    
+    {after_loop}
+    
+    /* Set depth value - from visvis TODO
+    int iter_depth = int(maxi);
+    // Calculate end position in world coordinates
+    vec4 position2 = vertexPosition;
+    position2.xyz += ray*shape*float(iter_depth);
+    // Project to device coordinates and set fragment depth
+    vec4 iproj = gl_ModelViewProjectionMatrix * position2;
+    iproj.z /= iproj.w;
+    gl_FragDepth = (iproj.z+1.0)/2.0;
+    */
+}}
+
+
+float rand(vec2 co)
+{{
+    // Create a pseudo-random number between 0 and 1.
+    // http://stackoverflow.com/questions/4200224
+    return fract(sin(dot(co.xy ,vec2(12.9898, 78.233))) * 43758.5453);
+}}
+
+float colorToVal(vec4 color1)
+{{
+    return color1.g; // todo: why did I have this abstraction in visvis?
+}}
+
+vec4 calculateColor(vec4 betterColor, vec3 loc, vec3 step)
+{{   
+    // Calculate color by incorporating lighting
+    vec4 color1;
+    vec4 color2;
+    
+    // View direction
+    vec3 V = normalize(view_ray);
+    
+    // calculate normal vector from gradient
+    vec3 N; // normal
+    color1 = $sample( u_volumetex, loc+vec3(-step[0],0.0,0.0) );
+    color2 = $sample( u_volumetex, loc+vec3(step[0],0.0,0.0) );
+    N[0] = colorToVal(color1) - colorToVal(color2);
+    betterColor = max(max(color1, color2),betterColor);
+    color1 = $sample( u_volumetex, loc+vec3(0.0,-step[1],0.0) );
+    color2 = $sample( u_volumetex, loc+vec3(0.0,step[1],0.0) );
+    N[1] = colorToVal(color1) - colorToVal(color2);
+    betterColor = max(max(color1, color2),betterColor);
+    color1 = $sample( u_volumetex, loc+vec3(0.0,0.0,-step[2]) );
+    color2 = $sample( u_volumetex, loc+vec3(0.0,0.0,step[2]) );
+    N[2] = colorToVal(color1) - colorToVal(color2);
+    betterColor = max(max(color1, color2),betterColor);
+    float gm = length(N); // gradient magnitude
+    N = normalize(N);
+    
+    // Flip normal so it points towards viewer
+    float Nselect = float(dot(N,V) > 0.0);
+    N = (2.0*Nselect - 1.0) * N;  // ==  Nselect * N - (1.0-Nselect)*N;
+    
+    // Get color of the texture (albeido)
+    color1 = betterColor;
+    color2 = color1;
+    // todo: parametrise color1_to_color2
+    
+    // Init colors
+    vec4 ambient_color = vec4(0.0, 0.0, 0.0, 0.0);
+    vec4 diffuse_color = vec4(0.0, 0.0, 0.0, 0.0);
+    vec4 specular_color = vec4(0.0, 0.0, 0.0, 0.0);
+    vec4 final_color;
+    
+    // todo: allow multiple light, define lights on viewvox or subscene
+    int nlights = 1; 
+    for (int i=0; i<nlights; i++)
+    {{ 
+        // Get light direction (make sure to prevent zero devision)
+        vec3 L = normalize(view_ray);  //lightDirs[i]; 
+        float lightEnabled = float( length(L) > 0.0 );
+        L = normalize(L+(1.0-lightEnabled));
+        
+        // Calculate lighting properties
+        float lambertTerm = clamp( dot(N,L), 0.0, 1.0 );
+        vec3 H = normalize(L+V); // Halfway vector
+        float specularTerm = pow( max(dot(H,N),0.0), u_shininess);
+        
+        // Calculate mask
+        float mask1 = lightEnabled;
+        
+        // Calculate colors
+        ambient_color +=  mask1 * u_ambient;  // * gl_LightSource[i].ambient;
+        diffuse_color +=  mask1 * lambertTerm;
+        specular_color += mask1 * specularTerm * u_specular;
+    }}
+    
+    // Calculate final color by componing different components
+    final_color = color2 * ( ambient_color + diffuse_color) + specular_color;
+    final_color.a = color2.a;
+    
+    // Done
+    return final_color;
+}}
+
+"""  # noqa
+
+
+MIP_SNIPPETS = dict(
+    before_loop="""
+        float maxval = -99999.0; // The maximum encountered value
+        int maxi = 0;  // Where the maximum value was encountered
+        """,
+    in_loop="""
+        if( val > maxval ) {
+            maxval = val;
+            maxi = iter;
+        }
+        """,
+    after_loop="""
+        // Refine search for max value
+        loc = start_loc + step * (float(maxi) - 0.5);
+        for (int i=0; i<10; i++) {
+            maxval = max(maxval, $sample(u_volumetex, loc).g);
+            loc += step * 0.1;
+        }
+        gl_FragColor = $cmap(maxval);
+        """,
+)
+MIP_FRAG_SHADER = FRAG_SHADER.format(**MIP_SNIPPETS)
+
+
+TRANSLUCENT_SNIPPETS = dict(
+    before_loop="""
+        vec4 integrated_color = vec4(0., 0., 0., 0.);
+        """,
+    in_loop="""
+            color = $cmap(val);
+            float a1 = integrated_color.a;
+            float a2 = color.a * (1 - a1);
+            float alpha = max(a1 + a2, 0.001);
+            
+            // Doesn't work.. GLSL optimizer bug?
+            //integrated_color = (integrated_color * a1 / alpha) + 
+            //                   (color * a2 / alpha); 
+            // This should be identical but does work correctly:
+            integrated_color *= a1 / alpha;
+            integrated_color += color * a2 / alpha;
+            
+            integrated_color.a = alpha;
+            
+            if( alpha > 0.99 ){
+                // stop integrating if the fragment becomes opaque
+                iter = nsteps;
+            }
+        
+        """,
+    after_loop="""
+        gl_FragColor = integrated_color;
+        """,
+)
+TRANSLUCENT_FRAG_SHADER = FRAG_SHADER.format(**TRANSLUCENT_SNIPPETS)
+
+
+ADDITIVE_SNIPPETS = dict(
+    before_loop="""
+        vec4 integrated_color = vec4(0., 0., 0., 0.);
+        """,
+    in_loop="""
+        color = $cmap(val);
+        
+        integrated_color = 1.0 - (1.0 - integrated_color) * (1.0 - color);
+        """,
+    after_loop="""
+        gl_FragColor = integrated_color;
+        """,
+)
+ADDITIVE_FRAG_SHADER = FRAG_SHADER.format(**ADDITIVE_SNIPPETS)
+
+
+ISO_SNIPPETS = dict(
+    before_loop="""
+        vec4 color3 = vec4(0.0);  // final color
+        vec3 dstep = 1.5 / u_shape;  // step to sample derivative
+    """,
+    in_loop="""
+        if (val > u_threshold-0.2) {
+            // Take the last interval in smaller steps
+            vec3 iloc = loc - step;
+            for (int i=0; i<10; i++) {
+                val = $sample(u_volumetex, iloc).g;
+                if (val > u_threshold) {
+                    color = $cmap(val);
+                    gl_FragColor = calculateColor(color, iloc, dstep);
+                    iter = nsteps;
+                    break;
+                }
+                iloc += step * 0.1;
+            }
+        }
+        """,
+    after_loop="""
+        """,
+)
+
+ISO_FRAG_SHADER = FRAG_SHADER.format(**ISO_SNIPPETS)
+
+frag_dict = {'mip': MIP_FRAG_SHADER, 'iso': ISO_FRAG_SHADER,
+             'translucent': TRANSLUCENT_FRAG_SHADER, 
+             'additive': ADDITIVE_FRAG_SHADER}
+
+
+class VolumeVisual(Visual):
+    """ Displays a 3D Volume
+    
+    Parameters
+    ----------
+    vol : ndarray
+        The volume to display. Must be ndim==2.
+    clim : tuple of two floats | None
+        The contrast limits. The values in the volume are mapped to
+        black and white corresponding to these values. Default maps
+        between min and max.
+    method : {'mip', 'translucent', 'additive', 'iso'}
+        The render method to use. See corresponding docs for details.
+        Default 'mip'.
+    threshold : float
+        The threshold to use for the isosurafce render method. By default
+        the mean of the given volume is used.
+    relative_step_size : float
+        The relative step size to step through the volume. Default 0.8.
+        Increase to e.g. 1.5 to increase performance, at the cost of
+        quality.
+    cmap : str
+        Colormap to use.
+    emulate_texture : bool
+        Use 2D textures to emulate a 3D texture. OpenGL ES 2.0 compatible,
+        but has lower performance on desktop platforms.
+    """
+
+    def __init__(self, vol, clim=None, method='mip', threshold=None, 
+                 relative_step_size=0.8, cmap='grays',
+                 emulate_texture=False):
+        Visual.__init__(self)
+        
+        # Only show back faces of cuboid. This is required because if we are 
+        # inside the volume, then the front faces are outside of the clipping
+        # box and will not be drawn.
+        self.set_gl_state('translucent', cull_face=False)
+        tex_cls = TextureEmulated3D if emulate_texture else Texture3D
+
+        # Storage of information of volume
+        self._vol_shape = ()
+        self._vertex_cache_id = ()
+        self._clim = None      
+
+        # Set the colormap
+        self._cmap = get_colormap(cmap)
+
+        # Create gloo objects
+        self._vbo = None
+        self._tex = tex_cls((10, 10, 10), interpolation='linear', 
+                            wrapping='clamp_to_edge')
+
+        # Create program
+        self._program = ModularProgram(VERT_SHADER)
+        self._program['u_volumetex'] = self._tex
+        self._index_buffer = None
+        
+        # Set data
+        self.set_data(vol, clim)
+        
+        # Set params
+        self.method = method
+        self.relative_step_size = relative_step_size
+        self.threshold = threshold if (threshold is not None) else vol.mean()
+    
+    def set_data(self, vol, clim=None):
+        """ Set the volume data. 
+
+        Parameters
+        ----------
+        vol : ndarray
+            The 3D volume.
+        clim : tuple | None
+            Colormap limits to use. None will use the min and max values.
+        """
+        # Check volume
+        if not isinstance(vol, np.ndarray):
+            raise ValueError('Volume visual needs a numpy array.')
+        if not ((vol.ndim == 3) or (vol.ndim == 4 and vol.shape[-1] <= 4)):
+            raise ValueError('Volume visual needs a 3D image.')
+        
+        # Handle clim
+        if clim is not None:
+            clim = np.array(clim, float)
+            if not (clim.ndim == 1 and clim.size == 2):
+                raise ValueError('clim must be a 2-element array-like')
+            self._clim = tuple(clim)
+        if self._clim is None:
+            self._clim = vol.min(), vol.max()
+        
+        # Apply clim
+        vol = np.array(vol, dtype='float32', copy=False)
+        vol -= self._clim[0]
+        vol *= 1.0 / (self._clim[1] - self._clim[0])
+        
+        # Apply to texture
+        self._tex.set_data(vol)  # will be efficient if vol is same shape
+        self._program['u_shape'] = vol.shape[2], vol.shape[1], vol.shape[0]
+        self._vol_shape = vol.shape[:3]
+        
+        # Create vertices?
+        if self._index_buffer is None:
+            self._create_vertex_data()
+    
+    @property
+    def clim(self):
+        """ The contrast limits that were applied to the volume data.
+        Settable via set_data().
+        """
+        return self._clim
+    
+    @property
+    def cmap(self):
+        return self._cmap
+
+    @cmap.setter
+    def cmap(self, cmap):
+        self._cmap = get_colormap(cmap)
+        self._program.frag['cmap'] = Function(self._cmap.glsl_map)
+        self.update()
+
+    @property
+    def method(self):
+        """The render method to use
+
+        Current options are:
+        
+            * translucent: voxel colors are blended along the view ray until
+              the result is opaque.
+            * mip: maxiumum intensity projection. Cast a ray and display the
+              maximum value that was encountered.
+            * additive: voxel colors are added along the view ray until
+              the result is saturated.
+            * iso: isosurface. Cast a ray until a certain threshold is
+              encountered. At that location, lighning calculations are
+              performed to give the visual appearance of a surface.  
+        """
+        return self._method
+    
+    @method.setter
+    def method(self, method):
+        # Check and save
+        known_methods = list(frag_dict.keys())
+        if method not in known_methods:
+            raise ValueError('Volume render method should be in %r, not %r' %
+                             (known_methods, method))
+        self._method = method
+        # Get rid of specific variables - they may become invalid
+        self._program['u_threshold'] = None
+
+        self._program.frag = frag_dict[method]
+        #self._program.frag['calculate_steps'] = Function(calc_steps)
+        self._program.frag['sampler_type'] = self._tex.glsl_sampler_type
+        self._program.frag['sample'] = self._tex.glsl_sample
+        self._program.frag['cmap'] = Function(self._cmap.glsl_map)
+        self.update()
+    
+    @property
+    def threshold(self):
+        """ The threshold value to apply for the isosurface render method.
+        """
+        return self._threshold
+    
+    @threshold.setter
+    def threshold(self, value):
+        self._threshold = float(value)
+        self.update()
+    
+    @property
+    def relative_step_size(self):
+        """ The relative step size used during raycasting.
+        
+        Larger values yield higher performance at reduced quality. If
+        set > 2.0 the ray skips entire voxels. Recommended values are
+        between 0.5 and 1.5. The amount of quality degredation depends
+        on the render method.
+        """
+        return self._relative_step_size
+    
+    @relative_step_size.setter
+    def relative_step_size(self, value):
+        value = float(value)
+        if value < 0.1:
+            raise ValueError('relative_step_size cannot be smaller than 0.1')
+        self._relative_step_size = value
+    
+    def _create_vertex_data(self):
+        """ Create and set positions and texture coords from the given shape
+        
+        We have six faces with 1 quad (2 triangles) each, resulting in
+        6*2*3 = 36 vertices in total.
+        """
+        
+        shape = self._vol_shape
+        
+        # Do we already have this or not?
+        vertex_cache_id = self._vol_shape
+        if vertex_cache_id == self._vertex_cache_id:
+            return
+        self._vertex_cache_id = None
+        
+        # Get corner coordinates. The -0.5 offset is to center
+        # pixels/voxels. This works correctly for anisotropic data.
+        x0, x1 = -0.5, shape[2] - 0.5
+        y0, y1 = -0.5, shape[1] - 0.5
+        z0, z1 = -0.5, shape[0] - 0.5
+
+        data = np.empty(8, dtype=[
+            ('a_position', np.float32, 3),
+            ('a_texcoord', np.float32, 3)
+        ])
+        
+        data['a_position'] = np.array([
+            [x0, y0, z0],
+            [x1, y0, z0],
+            [x0, y1, z0],
+            [x1, y1, z0],
+            [x0, y0, z1],
+            [x1, y0, z1],
+            [x0, y1, z1],
+            [x1, y1, z1],
+        ], dtype=np.float32)
+        
+        data['a_texcoord'] = np.array([
+            [0, 0, 0],
+            [1, 0, 0],
+            [0, 1, 0],
+            [1, 1, 0],
+            [0, 0, 1],
+            [1, 0, 1],
+            [0, 1, 1],
+            [1, 1, 1],
+        ], dtype=np.float32)
+        
+        """
+          6-------7
+         /|      /|
+        4-------5 |
+        | |     | |
+        | 2-----|-3
+        |/      |/
+        0-------1
+        """
+        
+        # Order is chosen such that normals face outward; front faces will be
+        # culled.
+        indices = np.array([2, 6, 0, 4, 5, 6, 7, 2, 3, 0, 1, 5, 3, 7],
+                           dtype=np.uint32)
+        
+        # Get some stats
+        self._kb_for_texture = np.prod(self._vol_shape) / 1024
+        self._kb_for_vertices = (indices.nbytes + data.nbytes) / 1024
+        
+        # Apply
+        if self._vbo is not None:
+            self._vbo.delete()
+            self._index_buffer.delete()
+        self._vbo = VertexBuffer(data)
+        self._program.bind(self._vbo)
+        self._index_buffer = IndexBuffer(indices)
+        self._vertex_cache_id = vertex_cache_id
+
+    def bounds(self, mode, axis):
+        """Get the visual bounds
+
+        Parameters
+        ----------
+        mode : str
+            The mode.
+        axis : int
+            The axis number.
+
+        Returns
+        -------
+        bounds : tuple
+            The lower and upper bounds.
+        """
+        # Not sure if this is right. Do I need to take the transform if this
+        # node into account?
+        # Also, this method has no docstring, and I don't want to repeat
+        # the docstring here. Maybe Visual implements _bounds that subclasses
+        # can implement?
+        return 0, self._vol_shape[2-axis]
+
+    def draw(self, transforms):
+        """Draw the visual
+
+        Parameters
+        ----------
+        transforms : instance of TransformSystem
+            The transforms to use.
+        """
+        Visual.draw(self, transforms)
+        
+        full_tr = transforms.get_full_transform()
+        self._program.vert['transform'] = full_tr
+        self._program['u_relative_step_size'] = self._relative_step_size
+        
+        # Get and set transforms
+        view_tr_f = transforms.visual_to_document
+        view_tr_i = view_tr_f.inverse
+        self._program.vert['viewtransformf'] = view_tr_f
+        self._program.vert['viewtransformi'] = view_tr_i
+        
+        # Set attributes that are specific to certain methods
+        self._program.build_if_needed()
+        if self._method == 'iso':
+            self._program['u_threshold'] = self._threshold
+        
+        # Draw!
+        self._program.draw('triangle_strip', self._index_buffer)
diff --git a/vispy/scene/visuals/xyz_axis.py b/vispy/visuals/xyz_axis.py
similarity index 72%
rename from vispy/scene/visuals/xyz_axis.py
rename to vispy/visuals/xyz_axis.py
index 09c4c83..0187221 100644
--- a/vispy/scene/visuals/xyz_axis.py
+++ b/vispy/visuals/xyz_axis.py
@@ -1,15 +1,15 @@
 
 import numpy as np
 
-from vispy.scene.visuals import Line
+from .line import LineVisual
 
 
-class XYZAxis(Line):
+class XYZAxisVisual(LineVisual):
     """
     Simple 3D axis for indicating coordinate system orientation. Axes are
     x=red, y=green, z=blue.
     """
-    def __init__(self, **kwds):
+    def __init__(self, **kwargs):
         verts = np.array([[0, 0, 0],
                           [1, 0, 0],
                           [0, 0, 0],
@@ -22,5 +22,5 @@ class XYZAxis(Line):
                           [0, 1, 0, 1],
                           [0, 0, 1, 1],
                           [0, 0, 1, 1]])
-        Line.__init__(self, pos=verts, color=color, connect='segments',
-                      mode='gl', **kwds)
+        LineVisual.__init__(self, pos=verts, color=color, connect='segments',
+                            method='gl', **kwargs)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/vispy.git



More information about the debian-science-commits mailing list