I've created a simple Perlin noise generator in JavaScript and am trying to find ways to optimize it. It lives inside a webpage and generates an image using nothing more than random number generator and a lot of arrays and algebra.
The end result is kind of slow. Not unusable, but a lot slower than a Java applet would have been. I'm wondering if I'm making some naive mistakes that could really speed things up or if JavaScript is not really up to this sort of task.
The code is on jsbin
function Random()
{
this.m = 2147483647; //2^31 - 1
this.a = 16807; //7^5; primitive root of m
this.q = 127773; // m / a
this.r = 2836; // m % a
this.seed = 1;
this.setSeed = function(seed)
{
if (seed <= 0)
{
seed = -(seed % (this.m - 1)) + 1;
}
if (seed > this.m - 1)
{
seed = this.m - 1;
}
this.seed = seed;
};
this.nextLong = function()
{
var res = this.a * (this.seed % this.q) - this.r * Math.floor(this.seed / this.q);
if (res <= 0)
{
res += this.m;
}
this.seed = res;
return res;
};
this.next = function()
{
var res = this.nextLong();
return res / this.m;
};
}
function Color(r, g, b, a)
{
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
function PerlinSampler2D(width, height, randseed)
{
this.width = width;
this.height = height;
this.randseed = randseed;
this.gradients = new Array(width * height * 2);
var rand = new Random();
rand.setSeed(randseed);
for (var i = 0; i < this.gradients.length; i += 2)
{
var x, y;
var angle = rand.next() * Math.PI * 2;
x = Math.sin(angle);
y = Math.cos(angle);
this.gradients[i] = x;
this.gradients[i + 1] = y;
}
this.dot = function(cellX, cellY, vx, vy)
{
var offset = (cellX + cellY * this.width) * 2;
var wx = this.gradients[offset];
var wy = this.gradients[offset + 1];
return wx * vx + wy * vy;
};
this.lerp = function(a, b, t)
{
return a + t * (b - a);
};
this.sCurve = function(t)
{
return t * t * (3 - 2 * t);
};
this.getValue = function(x, y)
{
var xCell = Math.floor(x);
var yCell = Math.floor(y);
var xFrac = x - xCell;
var yFrac = y - yCell;
var x0 = xCell;
var y0 = yCell;
var x1 = xCell === this.width - 1 ? 0 : xCell + 1;
var y1 = yCell === this.height - 1 ? 0 : yCell + 1;
var v00 = this.dot(x0, y0, xFrac, yFrac);
var v10 = this.dot(x1, y0, xFrac - 1, yFrac);
var v01 = this.dot(x0, y1, xFrac, yFrac - 1);
var v11 = this.dot(x1, y1, xFrac - 1, yFrac - 1);
var vx0 = this.lerp(v00, v10, this.sCurve(xFrac));
var vx1 = this.lerp(v01, v11, this.sCurve(xFrac));
return this.lerp(vx0, vx1, this.sCurve(yFrac));
};
}
function Image(width, height)
{
this.width = width;
this.height = height;
this.data = new Array(width * height * 4);
this.setColor = function(x, y, color)
{
var offset = ((y * this.width) + x) * 4;
this.data[offset] = color.r;
this.data[offset + 1] = color.g;
this.data[offset + 2] = color.b;
this.data[offset + 3] = color.a;
};
this.setRgba = function(x, y, r, g, b, a)
{
var offset = ((y * this.width) + x) * 4;
this.data[offset] = r;
this.data[offset + 1] = g;
this.data[offset + 2] = b;
this.data[offset + 3] = a;
};
this.createPerlinNoise = function(period, randseed)
{
var sampler = new PerlinSampler2D(Math.ceil(this.width / period), Math.ceil(this.height / period), randseed);
for (var j = 0; j < this.height; ++j)
{
for (var i = 0; i < this.width; ++i)
{
var val = sampler.getValue(i / period, j / period);
var b = (val + 1) / 2 * 256;
this.setRgba(i, j,
b, b, b, 0xff);
}
}
};
this.createTurbulence = function(period, levels, randseed)
{
var layers = new Array(this.width * this.height);
for (var j = 0; j < this.height; ++j)
{
for (var i = 0; i < this.width; ++i)
{
layers[i + j * this.width] = 0;
}
}
var localPeriod = period;
var atten = 1;
for (var k = 0; k < levels; ++k)
{
var sampler = new PerlinSampler2D(Math.ceil(this.width / localPeriod), Math.ceil(this.height / localPeriod), randseed);
for (var j = 0; j < this.height; ++j)
{
for (var i = 0; i < this.width; ++i)
{
var val = sampler.getValue(i / localPeriod, j / localPeriod);
layers[i + j * this.width] += val * atten;
}
}
localPeriod /= 2;
atten /= 2;
}
for (var j = 0; j < this.height; ++j)
{
for (var i = 0; i < this.width; ++i)
{
var b = (layers[i + j * this.width] + 1) / 2 * 256;
this.setRgba(i, j,
b, b, b, 0xff);
}
}
};
this.toImageContext = function(ctx)
{
var imgData = ctx.createImageData(this.width, this.height);
for (var i = 0, len = width * height * 4; i < len; i++)
{
imgData.data[i] = this.data[i];
}
return imgData;
};
}