/contourer

Visualise 2-input, 2-output functions the Contour way.

Primary LanguageJavaScriptMIT LicenseMIT

Contourer

Join the chat at https://gitter.im/krawthekrow/contourer

Electric field plot in Contour Grapher

See the demo here!

Contour graphing is an excellent way to visualise 2-input, 2-output functions.

What are 2-input, 2-output functions, you ask? Here are some examples:

  • 2x2 matrices transform 2D vectors to 2D vectors. (Lorentz Boost)
  • Map projections transform lat-long coordinates to a point in 2D space. (Mercator Warp, Mercator to Mollweide)
    • In Mercator (the most popular world map projection) to Mollweide (an equal-area projection), we see graphically how land area is distorted on world maps.
  • 2D electric fields assign a 2D field vector to every point in 2D space. (Dipole Formation, Attraction to Repulsion)
  • Neural networks with 2 input neurons and 2 output neurons map 2 real numbers to 2 real numbers. (Neural Network)
  • 2D classical physics maps the initial position of a particle in 2D space to its final position in said space. (Galaxy Warp)
  • Procedural terrain generation may assign a small 2D deformation to each point in 2D space for more interesting terrain. (Simplex Warp)
  • Complex functions transform a point on the complex plane (a complex number) to another point on the complex plane. (Everything else)

Contourer provides an easy way (well, provided you know GLSL) to plot any of these, and more!

Plotted something interesting that's not on the demo? Used my code in your own project? I'd love to see it!

Need help understanding the code? Think it can do with some refactoring? Have an awesome idea to share? Let's talk!

How to Plot

Important Note: If you want to see how a transformation warps the 2D space (like in computer graphics, or map projections), you need to plot the inverse of the function you're considering (see the examples in the demo). See How it Works if you're curious why. Or just trust me on this.

Plotting is done with the OpenGL ES 1.0 shading language. It's a C-like language for the GPU with native vector support.

The input coordinate is "cPos" (for current position) and the output coordinate is "res" (for result). For animations, the variable "time" is a uniform (global variable) that goes from 0 to 1 for each cycle.

Note that OpenGL ES does not implicitly cast ints to floats. You will need to write numbers like "23" as "23.0" to avoid compile errors.

OpenGL ES doesn't natively support complex numbers. The complex function examples do, however, provide a small complex number utility library that treats vec2s as complex numbers. You can access it under "Library Functions' on the demo page.

If you don't want to learn OpenGL ES, you can use the following snippet to transform it to effectively C code:

float inX = cPos.x, inY = cPos.y, outX, outY;
// Your code here, eg:
// float r = sqrt(inX * inX + inY * inY);
// float phi = time * 20.0 * atan(inY, inX);
// outX = r;
// outY = phi;
res = vec2(outX, outY);

If you're still unsure about how to code plot functions, use the examples in the demo as reference.

How it Works

You've probably heard of contour plots before, in the context of 2-input, 1-output functions like topographic maps. This is achieved by colouring in every point where the function takes on an integer value.

Contourer does precisely that, except for 2-input, 2-output functions. This is equivalent to contouring two separate 2-input, 1-output functions and overlaying one plot over the other. Unfortunately, overlaying contour plots is a very rare use case.

More commonly, we want to see how a 2-input, 2-output function transforms space. Amazingly, contour plots can help us with that.

To understand how, we'll first consider how we could visualise a function transforming an image. If our function were y = f(x), then the pixel at position x would be mapped to position y on the screen.

We could position each image pixel on the screen in this manner, but it would be more efficient to do it backwards: for each screen pixel y, we use the inverse function to find the corresponding position x in the image, and sample the image there.

In Contourer's case, we want to draw an infinite grid, so there's no image to sample. Instead, the grid is dynamically generated with another shader program.

To generate a grid, we would ideally want to colour all points that correspond to an image position with an integral x- or y-coordinate. However, the image coordinates we sample would rarely attain exactly integer values. To get around this, we compare the coordinates at each pixel with that of neighbouring pixels, and see if an integer lies between any pair.

If this is confusing, here's a step-by-step example. Let's say we want to see the effects of the function y = 10 * (x - (1.05, 1.05)), which is a translation followed by a scaling. Inverting, we get:

res = cPos * 0.1 + vec2(1.05, 1.05);

Now consider the following nine points in screen space:

(9,  9) (10,  9) (11,  9)
(9, 10) (10, 10) (11, 10)
(9, 11) (10, 11) (11, 11)

Using the inverse function, these points correspond to the following points in "grid space":

(1.95, 1.95) (2.05, 1.95) (2.15, 1.95)
(1.95, 2.05) (2.05, 2.05) (2.15, 2.05)
(1.95, 2.15) (2.05, 2.15) (2.15, 2.15)

We colour a pixel if at least one of its neighbours has an x-coordinate less than its own x-coordinate, and there exists an integer somewhere in between. This leads us to colour the middle column: for each pixel in that column, the pixel on its left satisfies the condition. Doing the same for the y-coordinate, we colour the middle row.

Observe that this effectively results in two overlaid contour plots. Now if only there were an easy way to find inverses for at least some special classes of functions...

Technical Details

The graphing is GPU-accelerated with WebGL. I originally intended to use the floating point textures (OES_texture_float) extension, but this didn't work reliably on Chrome. In the end, I reverted to a float-packing technique.

Heavy inspiration was taken from the WebGL GPGPU libraries gpu.js and WebCLGL, but ultimately I had to reimplement it to meet my specific needs.

The UI is implemented with Bootstrap, React and ACE editor.

How to Build

Download the source, install build dependencies and start babel watch:

git clone https://github.com/krawthekrow/contourer/
cd contourer
npm install

# In another window
npm run babelWatch

Any modifications you make would immediately be seen in index-dev.html. Compile and minify the source to see changes in index.html:

gulp build

Tests

Uhh... sorry? Did you say something? Oh whoops, I'm late for an appointment. Bye!

License

Contourer is released under the MIT license.

Contact Me

Drop me an email!
Chat me up on Gitter!
Or if you really have to, mention me on Twitter...