In this post I want to describe simple methodology of pixel image distortion on “clean” javascript in 2D – Canvas without the use of the special libraries and shaders, by the direct access to the pixels of the image. I hope, it will be interesting and useful both for general development and for the decision of some tasks.
Canvas and pixels
I will not describe the object of Canvas fully, there is a certain documentation for this purpose. We will stop on the issues that are interesting to us. Firstly, it’s the receipt of 2D context:
var context = canvas.getContext('2d');
This context is able to do great things with two-dimensional graphic, including the ability to get the direct access to the pixels in the set area:
var pixels = context.getImageData(x, y, width, height); context.putImageData(pixels, x, y);
And we are going to change these pixels. We will examine 32-bit images only. Every pixel of such image is four bytes, a byte for a channel(R, G, B, A). Pixels are a unidimensional array of these bytes. One can access them through the data field (x, y are coordinates, c is a channel, b is a value):
pixels.data[(x+y*height)*4+c] = b;
Function of distortion
Distortions of the image that we examine is a function the parameters of which are coordinates of the image we get (further we will call them pixels), and the result of which are coordinates of the initial image (further we will call them texels, because actually an initial image is a texture, and coordinates are numbers with floating point). Thus, a function for the increase of the image has approximately next look:
var zoom = function(px, py) { return { 'x': (px+width/2)*0.5, 'y': (py+height/2)*0.5 } }
We will make a few functions for other distortions. I don’t see any point to describe every algorithm, mathematics is simple enough and talks for itself.
var twirl = function(px, py) { var x = px-width/2; var y = py-height/2; var r = Math.sqrt(x*x+y*y); var maxr = width/2; if (r>maxr) return { 'x':px, 'y':py } var a = Math.atan2(y,x); a += 1-r/maxr; var dx = Math.cos(a)*r; var dy = Math.sin(a)*r; return { 'x': dx+width/2, 'y': dy+height/2 } }
var reflect = function(px, py) { if (py<height/2) return { 'x': px, 'y': py } var dx = (py-height/2)*(-px+width/2)/width; return { 'x': px+dx, 'y': height-py } }
var spherize = function(px,py) { var x = px-width/2; var y = py-height/2; var r = Math.sqrt(x*x+y*y); var maxr = width/2; if (r>maxr) return { 'x':px, 'y':py } var a = Math.atan2(y,x); var k = (r/maxr)*(r/maxr)*0.5+0.5; var dx = Math.cos(a)*r*k; var dy = Math.sin(a)*r*k; return { 'x': dx+width/2, 'y': dy+height/2 } }
Hash-table
So, we got the possibility to know, what texels to take for every pixel. We shouldn’t calculate the coordinates each time, should we? It will be a hard way. For this purpose a hash-table comes for help. Thus, we calculate all the transformations for every size of image singly, and in the future we’ll use it for every transformation:
// A parameter is a translator function. If it is a line, a function is set from the available functions in an object. var setTranslate = function(translator) { if (typeof translator === 'string') translator = this[translator]; for (var y=0; y<height; y++) { for (var x=0; x<width; x++) { var t = translator(x, y); map[(x+y*height)*2+0] = Math.max(Math.min(t.x, width-1), 0); map[(x+y*height)*2+1] = Math.max(Math.min(t.y, height-1), 0); } } }
Bilinear filtering
In order not to spoil our mood with sharp borders, we will apply the classic algorithm of bilinear filtration. More detailed information you can find on Wikipedia. The essence of the algorithm consists in finding a color of pixel depending on the four nearest texels. In our case, the algorithm will look like this:
var colorat = function(x, y, channel) { return texture.data[(x+y*height)*4+channel]; } for (var j=0; j<height; j++) { for (var i=0; i<width; i++) { var u = map[(i+j*height)*2]; var v = map[(i+j*height)*2+1]; var x = Math.floor(u); var y = Math.floor(v); var kx = u-x; var ky = v-y; for (var c=0; c<4; c++) { bitmap.data[(i+j*height)*4+c] = (colorat(x, y , c)*(1-kx) + colorat(x+1, y , c)*kx) * (1-ky) + (colorat(x, y+1, c)*(1-kx) + colorat(x+1, y+1, c)*kx) * (ky); } } }
Conclusion
That’s actually all. The only thing remain is turning it in a separate object, adding it to the code and see what you get.
You can try it in real time on JSFiddle. Works in Chrome and Firefox.
Thank you for your attention.
p.s. You can read this article on Russian.