I'm trying to understand how image resampling methods work. I've read/watched several pages/videos and I think I got the idea. However, I couldn't find any working example on how to implement it. So I thought I should start with the basics: nearest neighbor resampling on 1D.
This was very straightforward and I think I got it. JSFiddle Demo.
function resample() {
var widthScaled = Math.round(originalPixels.width * scaleX);
var sampledPixels = context.createImageData(widthScaled, originalPixels.height);
for (var i = 0; i < sampledPixels.data.length; i+=4) {
var position = index2pos(sampledPixels, i);
var origPosX = Math.floor(position.x / scaleX);
var origColor = getPixel(originalPixels, origPosX, position.y);
setPixel(sampledPixels, position.x, position.y, origColor);
}
loadImage(context, sampledPixels);
}
Next, I moved on to linear interpolation. Thought it'd be simple too, but I'm having problems. First, how do I deal with the last pixel (marked red)? It has only one neighboring pixel. Second, my result is too sharp when compared to Photoshop's. Is my method flawed, or is PS doing some extra work? JSFiddle Demo.
function resample() {
var sampledPixels = context.createImageData(originalPixels.width * scaleX, originalPixels.height);
for (var i = 0; i < sampledPixels.data.length; i+=4) {
var position = index2pos(sampledPixels, i);
var origPosX = position.x / scaleX;
var leftPixelPosX = Math.floor(origPosX);
var rightPixelPosX = Math.ceil(origPosX);
var leftPixelColor = getPixel(originalPixels, leftPixelPosX, position.y);
var rightPixelColor = getPixel(originalPixels, rightPixelPosX, position.y);
var weight = origPosX % 1;
var color = mix(leftPixelColor[0], rightPixelColor[0], weight);
color = [color, color, color, 255];
setPixel(sampledPixels, position.x, position.y, color);
}
loadImage(context, sampledPixels);
}
function mix(x, y, a) {
return x * (1 - a) + y * a;
}
Linear interpolation of pixels
There is no real right and wrong way to do filtering, as the result is subjective and the quality of the result is up to you, Is it good enough, or do you feel there is room for improvement.
There are also a wide variety of filtering methods, nearest neighbor, linear, bilinear, polynomial, spline, Lanczos... and each can have many variations. There are also factors like what is the filtering output format; screen, print, video. Is quality prefered over speed, or memory efficiency. And why upscale when hardware will do it for you in real-time anyways.
It looks like you have the basics of linear filtering correct
Update Correction. Linear and bilinear refer to the same type of interpolation, bilinear is 2D and linear is 1D
Handling the last Pixel
In the case of the missing pixel there are several options,
Assume the colour continues so just copy the last pixel.
Assume the next pixel is the background, border colour, or some predefined edge colour.
Wrap around to the pixel at the other side (best option for tile maps)
If you know there is a background image use its pixels
Just drop the last pixel (image size will be 1 pixel smaller)
The PS result
To me the PhotoShop result looks like a form of bilinear filtering, though it should be keeping the original pixel colours, so something a little more sophisticated is being used. Without knowing what the method is you will have a hard time matching it.
A spectrum for best results
Good filtering will find the spectrum of frequencies at a particular point and reconstruct the missing pixel based on that information.
If you think of a line of pixels not as values but as volume then a line of pixels makes a waveform. Any complex waveform can be broken down into a set of simpler basic pure tones (frequencies). You can then get a good approximation by adding all the frequencies at a particular point.
Filters that use this method are usually denoted with Fourier, or FFT (Fast Fourier Transform) and require a significant amount of process over standard linear interpolation.
What RGB values represent.
Each channel red, green, and blue represent the square root of that channel's intensity/brightness. (this is a close general purpose approximation) Thus when you interpolate you need to convert to the correct values then interpolate then convert back to the logarithmic values.
Correct interpolation
function interpolateLinear(pos,c1,c2){ // pos 0-1, c1,c2 are objects {r,g,b}
return {
r : Math.sqrt((c2.r * c2.r + c1.r * c1.r) * pos + c1.r * c1.r),
g : Math.sqrt((c2.g * c2.g + c1.g * c1.g) * pos + c1.g * c1.g),
b : Math.sqrt((c2.b * c2.b + c1.b * c1.b) * pos + c1.b * c1.b),
};
}
It is important to note that the vast majority of digital processing software does not correctly interpolate. This is in part due to developers ignorance of the output format (why I harp on about it when I can), and partly due to compliance with ye olde computers that struggled just to display an image let alone process it (though I don't buy that excuse).
HTML5 is no exception and incorrectly interpolates pixel values in almost all interpolations. This producing dark bands where there is strong hue contrast and darker total brightness for up and down scaled image. Once you notice the error it will forever annoy you as today's hardware is easily up to the job.
To illustrate just how bad incorrect interpolation can be the following image shows the correct (top) and the canvas 2D API using a SVG filter (bottom) interpolation.
2D linear interpolation (Bilinear)
Interpolating along both axis is done by doing each axis in turn. First interpolate along the x axis and then along the y axis. You can do this as a 2 pass process or a single pass.
The following function will interpolate at any sub pixel coordinate. This function is not built for speed and there is plenty of room for optimisation.
// Get pixel RGBA value using bilinear interpolation.
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgDat.width) * 4;
var i2 = (ix2 + iy1 * imgDat.width) * 4;
var i3 = (ix1 + iy2 * imgDat.width) * 4;
var i4 = (ix2 + iy2 * imgDat.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
for(i = 0; i < 3; i ++){
// interpolate x for top and bottom pixels
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const upScale = 4;
// usage
const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const imgData2 = ctx.createImageData(ctx.canvas.width * upScale, ctx.canvas.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgdata2.width) * 4);
}
}
Example upscale canvas 8 times
The example uses the above function to upscale a test pattern by 8. Three images are displayed. The original 64 by 8 then, the computed upscale using logarithmic bilinear interpolation, and then using the canvas standard API drawImage to upScale (Using the default interpolation, bilinear) .
// helper functions create canvas and get context
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// iterators
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
const upScale = 8;
var canvas1 = CImageCtx(64,8);
var canvas2 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
var canvas3 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y) | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgDat.width) * 4;
var i2 = (ix2 + iy1 * imgDat.width) * 4;
var i3 = (ix1 + iy2 * imgDat.width) * 4;
var i4 = (ix2 + iy2 * imgDat.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
// interpolate x for top and bottom pixels
for(i = 0; i < 3; i ++){
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const ctx = canvas1.ctx;
var cols = ["black","red","green","Blue","Yellow","Cyan","Magenta","White"];
doFor(8,j => eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(j*8+i,0,1,8)}));
eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(i * 8,4,8,4)});
const imgData = ctx.getImageData(0, 0, canvas1.width, canvas1.height);
const imgData2 = ctx.createImageData(canvas1.width * upScale, canvas1.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgData2.width) * 4);
}
}
canvas2.ctx.putImageData(imgData2,0,0);
function $(el,text){const e = document.createElement(el); e.textContent = text; document.body.appendChild(e)};
document.body.appendChild(canvas1);
$("div","Next Logarithmic upscale using linear interpolation * 8");
document.body.appendChild(canvas2);
canvas3.ctx.drawImage(canvas1,0,0,canvas3.width,canvas3.height);
document.body.appendChild(canvas3);
$("div","Previous Canvas 2D API upscale via default linear interpolation * 8");
$("div","Note the overall darker result and dark lines at hue boundaries");
canvas { border : 2px solid black; }
I'm using JsIso (found it on github) to (hopefully) make a fun little browser game. I modified the hardcoded values for a height map, into a variable and function to generate terrain randomly. What I would like to do, but can't picture in my head at all, is to have a given tile no more or less than 2 levels different than the tile next to it, getting rid of towers and pits.
This is my current code:
var tileHeightMap = generateGround(10, 10); //Simple usage
function generateGround(height, width)
{
var ground = [];
for (var y = 0 ; y < height; y++)
{
ground[y] = [];
for (var x = 0; x < width; x++)
{
ground[y][x] = tile();
}
}
return ground;
function tile()
{
return (Math.random() * 5 | 0);
}
}
It looks like it would be best to modify the tile function, perhaps passing it the value of the previous tile, and not the generate ground function. If more info is needed, let me know!
You can use a two-dimensional Value Noise.
It basically works like this:
Octave #1: Create a number of random points (8, for example) that are evenly spaced in x direction and interpolate between them (if you choose linear interpolation, it could look like this):
Octave #2: Do the same thing as in #1, but double the amount of points. The amplitude should be the half of the amplitude in #1. Now interpolate again and add the values from both octaves together.
Octave #3: Do the same thing as in #2, but with the double amount of points and an amplitude that is the half of the amplitude in #2.
Continue these steps as long as you want.
This creates a one-dimensional Value Noise. The following code generates a 2d Value Noise and draws the generated map to the canvas:
function generateHeightMap (width, height, min, max) {
const heightMap = [], // 2d array containing the heights of the tiles
octaves = 4, // 4 octaves
startFrequencyX = 2,
startFrequencyY = 2;
// linear interpolation function, could also be cubic, trigonometric, ...
const interpolate = (a, b, t) => (b - a) * t + a;
let currentFrequencyX = startFrequencyX, // specifies how many points should be generated in this octave
currentFrequencyY = startFrequencyY,
currentAlpha = 1, // the amplitude
octave = 0,
x = 0,
y = 0;
// fill the height map with zeros
for (x = 0 ; x < width; x += 1) {
heightMap[x] = [];
for (y = 0; y < height; y += 1) {
heightMap[x][y] = 0;
}
}
// main loop
for (octave = 0; octave < octaves; octave += 1) {
if (octave > 0) {
currentFrequencyX *= 2; // double the amount of point
currentFrequencyY *= 2;
currentAlpha /= 2; // take the half of the amplitude
}
// create random points
const discretePoints = [];
for (x = 0; x < currentFrequencyX + 1; x += 1) {
discretePoints[x] = [];
for (y = 0; y < currentFrequencyY + 1; y += 1) {
// create a new random value between 0 and amplitude
discretePoints[x][y] = Math.random() * currentAlpha;
}
}
// now interpolate and add to the height map
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
const currentX = x / width * currentFrequencyX,
currentY = y / height * currentFrequencyY,
indexX = Math.floor(currentX),
indexY = Math.floor(currentY),
// interpolate between the 4 neighboring discrete points (2d interpolation)
w0 = interpolate(discretePoints[indexX][indexY], discretePoints[indexX + 1][indexY], currentX - indexX),
w1 = interpolate(discretePoints[indexX][indexY + 1], discretePoints[indexX + 1][indexY + 1], currentX - indexX);
// add the value to the height map
heightMap[x][y] += interpolate(w0, w1, currentY - indexY);
}
}
}
// normalize the height map
let currentMin = 2; // the highest possible value at the moment
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
if (heightMap[x][y] < currentMin) {
currentMin = heightMap[x][y];
}
}
}
// currentMin now contains the smallest value in the height map
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
heightMap[x][y] -= currentMin;
}
}
// now, the minimum value is guaranteed to be 0
let currentMax = 0;
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
if (heightMap[x][y] > currentMax) {
currentMax = heightMap[x][y];
}
}
}
// currentMax now contains the highest value in the height map
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
heightMap[x][y] /= currentMax;
}
}
// the values are now in a range from 0 to 1, modify them so that they are between min and max
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
heightMap[x][y] = heightMap[x][y] * (max - min) + min;
}
}
return heightMap;
}
const map = generateHeightMap(40, 40, 0, 2); // map size 40x40, min=0, max=2
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
for (let x = 0; x < 40; x += 1) {
for (let y = 0; y < 40; y += 1) {
const height = map[x][y];
ctx.fillStyle = 'rgb(' + height * 127 + ', 127, 127)';
// draw the tile (tile size 5x5)
ctx.fillRect(x * 5, y * 5, 5, 5);
}
}
<canvas width="200" height="200"></canvas>
Note that the values in this height map can reach from -2 to 2. To change that, change the method that is used to create the random values.
Edit:
I made a mistake there, the version before the edit reached from -1 to 1. I modified it so that you can easily specify the minimum and maximum value.
First, I normalize the height map so that the values really reach from 0 to 1. Then, I modify all values so that they are between the specified min and max value.
Also, I changed how the heights are displayed. Instead of land and water, it now displays a smooth noise. The more red a point contains, the higher it is.
By the way, this algorithm is widely used in Procedural Content Generation for games.
If you want further explanation, just ask!
In my index.js file I have the following code to link to my node_module:
var createTerrain = require('voxel-perlin-terrain');
window.generator = createTerrain('abcxyz', 0, 25)
This allows me to use a seed, a min amount, and a max amount respectively. However this only gives me one 2d plane of noise, I'd like to have it be 3d. I noticed something towards the end of the node module though:
/*
* A speed-improved perlin and simplex noise algorithms for 2D.
*
* Based on example code by Stefan Gustavson (stegu#itn.liu.se).
* Optimisations by Peter Eastman (peastman#drizzle.stanford.edu).
* Better rank ordering method by Stefan Gustavson in 2012.
* Converted to Javascript by Joseph Gentle.
*
* Version 2012-03-09
*
* This code was placed in the public domain by its original author,
* Stefan Gustavson. You may use it as you see fit, but
* attribution is appreciated.
*
*/
(function(global){
var module = global.noise = {};
function Grad(x, y, z) {
this.x = x; this.y = y; this.z = z;
}
Grad.prototype.dot2 = function(x, y) {
return this.x*x + this.y*y;
};
Grad.prototype.dot3 = function(x, y, z) {
return this.x*x + this.y*y + this.z*z;
};
var grad3 = [new Grad(1,1,0),new Grad(-1,1,0),new Grad(1,-1,0),new Grad(-1,-1,0),
new Grad(1,0,1),new Grad(-1,0,1),new Grad(1,0,-1),new Grad(-1,0,-1),
new Grad(0,1,1),new Grad(0,-1,1),new Grad(0,1,-1),new Grad(0,-1,-1)];
var p = [151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180];
// To remove the need for index wrapping, double the permutation table length
var perm = new Array(512);
var gradP = new Array(512);
// This isn't a very good seeding function, but it works ok. It supports 2^16
// different seed values. Write something better if you need more seeds.
module.seed = function(seed) {
if(seed > 0 && seed < 1) {
// Scale the seed out
seed *= 65536;
}
seed = Math.floor(seed);
if(seed < 256) {
seed |= seed << 8;
}
for(var i = 0; i < 256; i++) {
var v;
if (i & 1) {
v = p[i] ^ (seed & 255);
} else {
v = p[i] ^ ((seed>>8) & 255);
}
perm[i] = perm[i + 256] = v;
gradP[i] = gradP[i + 256] = grad3[v % 12];
}
};
module.seed(0);
/*
for(var i=0; i<256; i++) {
perm[i] = perm[i + 256] = p[i];
gradP[i] = gradP[i + 256] = grad3[perm[i] % 12];
}*/
// Skewing and unskewing factors for 2, 3, and 4 dimensions
var F2 = 0.5*(Math.sqrt(3)-1);
var G2 = (3-Math.sqrt(3))/6;
var F3 = 1/3;
var G3 = 1/6;
// 2D simplex noise
module.simplex2 = function(xin, yin) {
var n0, n1, n2; // Noise contributions from the three corners
// Skew the input space to determine which simplex cell we're in
var s = (xin+yin)*F2; // Hairy factor for 2D
var i = Math.floor(xin+s);
var j = Math.floor(yin+s);
var t = (i+j)*G2;
var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed.
var y0 = yin-j+t;
// For the 2D case, the simplex shape is an equilateral triangle.
// Determine which simplex we are in.
var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
if(x0>y0) { // lower triangle, XY order: (0,0)->(1,0)->(1,1)
i1=1; j1=0;
} else { // upper triangle, YX order: (0,0)->(0,1)->(1,1)
i1=0; j1=1;
}
// A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
// a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
// c = (3-sqrt(3))/6
var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
var y1 = y0 - j1 + G2;
var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords
var y2 = y0 - 1 + 2 * G2;
// Work out the hashed gradient indices of the three simplex corners
i &= 255;
j &= 255;
var gi0 = gradP[i+perm[j]];
var gi1 = gradP[i+i1+perm[j+j1]];
var gi2 = gradP[i+1+perm[j+1]];
// Calculate the contribution from the three corners
var t0 = 0.5 - x0*x0-y0*y0;
if(t0<0) {
n0 = 0;
} else {
t0 *= t0;
n0 = t0 * t0 * gi0.dot2(x0, y0); // (x,y) of grad3 used for 2D gradient
}
var t1 = 0.5 - x1*x1-y1*y1;
if(t1<0) {
n1 = 0;
} else {
t1 *= t1;
n1 = t1 * t1 * gi1.dot2(x1, y1);
}
var t2 = 0.5 - x2*x2-y2*y2;
if(t2<0) {
n2 = 0;
} else {
t2 *= t2;
n2 = t2 * t2 * gi2.dot2(x2, y2);
}
// Add contributions from each corner to get the final noise value.
// The result is scaled to return values in the interval [-1,1].
return 70 * (n0 + n1 + n2);
};
// 3D simplex noise
module.simplex3 = function(xin, yin, zin) {
var n0, n1, n2, n3; // Noise contributions from the four corners
// Skew the input space to determine which simplex cell we're in
var s = (xin+yin+zin)*F3; // Hairy factor for 2D
var i = Math.floor(xin+s);
var j = Math.floor(yin+s);
var k = Math.floor(zin+s);
var t = (i+j+k)*G3;
var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed.
var y0 = yin-j+t;
var z0 = zin-k+t;
// For the 3D case, the simplex shape is a slightly irregular tetrahedron.
// Determine which simplex we are in.
var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords
var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords
if(x0 >= y0) {
if(y0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=1; k2=0; }
else if(x0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=0; k2=1; }
else { i1=0; j1=0; k1=1; i2=1; j2=0; k2=1; }
} else {
if(y0 < z0) { i1=0; j1=0; k1=1; i2=0; j2=1; k2=1; }
else if(x0 < z0) { i1=0; j1=1; k1=0; i2=0; j2=1; k2=1; }
else { i1=0; j1=1; k1=0; i2=1; j2=1; k2=0; }
}
// A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z),
// a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and
// a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where
// c = 1/6.
var x1 = x0 - i1 + G3; // Offsets for second corner
var y1 = y0 - j1 + G3;
var z1 = z0 - k1 + G3;
var x2 = x0 - i2 + 2 * G3; // Offsets for third corner
var y2 = y0 - j2 + 2 * G3;
var z2 = z0 - k2 + 2 * G3;
var x3 = x0 - 1 + 3 * G3; // Offsets for fourth corner
var y3 = y0 - 1 + 3 * G3;
var z3 = z0 - 1 + 3 * G3;
// Work out the hashed gradient indices of the four simplex corners
i &= 255;
j &= 255;
k &= 255;
var gi0 = gradP[i+ perm[j+ perm[k ]]];
var gi1 = gradP[i+i1+perm[j+j1+perm[k+k1]]];
var gi2 = gradP[i+i2+perm[j+j2+perm[k+k2]]];
var gi3 = gradP[i+ 1+perm[j+ 1+perm[k+ 1]]];
// Calculate the contribution from the four corners
var t0 = 0.5 - x0*x0-y0*y0-z0*z0;
if(t0<0) {
n0 = 0;
} else {
t0 *= t0;
n0 = t0 * t0 * gi0.dot3(x0, y0, z0); // (x,y) of grad3 used for 2D gradient
}
var t1 = 0.5 - x1*x1-y1*y1-z1*z1;
if(t1<0) {
n1 = 0;
} else {
t1 *= t1;
n1 = t1 * t1 * gi1.dot3(x1, y1, z1);
}
var t2 = 0.5 - x2*x2-y2*y2-z2*z2;
if(t2<0) {
n2 = 0;
} else {
t2 *= t2;
n2 = t2 * t2 * gi2.dot3(x2, y2, z2);
}
var t3 = 0.5 - x3*x3-y3*y3-z3*z3;
if(t3<0) {
n3 = 0;
} else {
t3 *= t3;
n3 = t3 * t3 * gi3.dot3(x3, y3, z3);
}
// Add contributions from each corner to get the final noise value.
// The result is scaled to return values in the interval [-1,1].
return 32 * (n0 + n1 + n2 + n3);
};
// ##### Perlin noise stuff
function fade(t) {
return t*t*t*(t*(t*6-15)+10);
}
function lerp(a, b, t) {
return (1-t)*a + t*b;
}
// 2D Perlin Noise
module.perlin2 = function(x, y) {
// Find unit grid cell containing point
var X = Math.floor(x), Y = Math.floor(y);
// Get relative xy coordinates of point within that cell
x = x - X; y = y - Y;
// Wrap the integer cells at 255 (smaller integer period can be introduced here)
X = X & 255; Y = Y & 255;
// Calculate noise contributions from each of the four corners
var n00 = gradP[X+perm[Y]].dot2(x, y);
var n01 = gradP[X+perm[Y+1]].dot2(x, y-1);
var n10 = gradP[X+1+perm[Y]].dot2(x-1, y);
var n11 = gradP[X+1+perm[Y+1]].dot2(x-1, y-1);
// Compute the fade curve value for x
var u = fade(x);
// Interpolate the four results
return lerp(
lerp(n00, n10, u),
lerp(n01, n11, u),
fade(y));
};
// 3D Perlin Noise
module.perlin3 = function(x, y, z) {
// Find unit grid cell containing point
var X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z);
// Get relative xyz coordinates of point within that cell
x = x - X; y = y - Y; z = z - Z;
// Wrap the integer cells at 255 (smaller integer period can be introduced here)
X = X & 255; Y = Y & 255; Z = Z & 255;
// Calculate noise contributions from each of the eight corners
var n000 = gradP[X+ perm[Y+ perm[Z ]]].dot3(x, y, z);
var n001 = gradP[X+ perm[Y+ perm[Z+1]]].dot3(x, y, z-1);
var n010 = gradP[X+ perm[Y+1+perm[Z ]]].dot3(x, y-1, z);
var n011 = gradP[X+ perm[Y+1+perm[Z+1]]].dot3(x, y-1, z-1);
var n100 = gradP[X+1+perm[Y+ perm[Z ]]].dot3(x-1, y, z);
var n101 = gradP[X+1+perm[Y+ perm[Z+1]]].dot3(x-1, y, z-1);
var n110 = gradP[X+1+perm[Y+1+perm[Z ]]].dot3(x-1, y-1, z);
var n111 = gradP[X+1+perm[Y+1+perm[Z+1]]].dot3(x-1, y-1, z-1);
// Compute the fade curve value for x, y, z
var u = fade(x);
var v = fade(y);
var w = fade(z);
// Interpolate
return lerp(
lerp(
lerp(n000, n100, u),
lerp(n001, n101, u), w),
lerp(
lerp(n010, n110, u),
lerp(n011, n111, u), w),
v);
};
})(typeof module === "undefined" ? this : module.exports);
There's a "3D Perlin Noise" module.perlin3 line at the beginning of the final function. Is there some way of utilizing that?
for this project http://biduleohm.free.fr/ledohm/ (sorry, the user interface is in french but the code is in english) I need an angular gradient but it doesn't exists in native so I've implemented it using a linear gradient on a line and I draw the lines more and more longer to form a triangle. The result is graphically OK but the speed isn't really good (1850 ms for 125 triangles). It's in the tab [Répartition], it redraws if there is a keyup event on one of the inputs, don't be afraid of the apparent slowness, I've limited to maximum one redraw every 2000 ms.
Before I used a simple linear gradient on the whole triangle (but this doesn't match the reality) and the speed was OK, it draws thousands of triangles in less than a second. This function was used :
drawFrontLightForColor : function(x, y, w, h, color) {
var x2 = x - w;
var x3 = x + w;
var gradient = Distri.frontCanvas.createLinearGradient(x2, y, x3, y);
gradient.addColorStop(0, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
gradient.addColorStop(0.5, 'rgba(' + color + ', ' + (color == Distri.lightColors.cw ? Distri.lightCenterAlphaCw : Distri.lightCenterAlphaOther) + ')');
gradient.addColorStop(1, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
Distri.frontCanvas.fillStyle = gradient;
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x, y);
Distri.frontCanvas.lineTo(x2, (y + h));
Distri.frontCanvas.lineTo(x3, (y + h));
Distri.frontCanvas.lineTo(x, y);
Distri.frontCanvas.fill();
Distri.frontCanvas.closePath();
},
Then I switched to this function :
drawFrontLightForColor : function(x, y, w, h, centerColor, edgeColor) {
var ratio = w / h;
var tmpY;
var tmpW;
var x2;
var x3;
var gradient;
Distri.frontCanvas.lineWidth = 1;
for (var tmpH = 0; tmpH < h; tmpH++) {
tmpY = y + tmpH;
tmpW = Math.round(tmpH * ratio);
x2 = x - tmpW;
x3 = x + tmpW;
gradient = Distri.frontCanvas.createLinearGradient(x2, tmpY, x3, tmpY);
gradient.addColorStop(0, edgeColor);
gradient.addColorStop(0.5, centerColor);
gradient.addColorStop(1, edgeColor);
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x2, tmpY);
Distri.frontCanvas.lineTo(x, tmpY);
Distri.frontCanvas.lineTo(x3, tmpY);
Distri.frontCanvas.strokeStyle = gradient;
Distri.frontCanvas.stroke();
Distri.frontCanvas.closePath();
}
},
You can find the whole source here
I can't put the beginPath, stroke, closePath out of the loop because of the gradient which is changing every iteration (I've tried but it used the last gradient for every line (which, ironically, is identical to the first function...) which is understandable but not what I want).
I accept any advice (including redo the whole function and modify his caller to outsource some code) to improve the speed let's say 5x (ideally more).
I think you took the wrong way from the start : when doing so much changes of color, you have better operate at the pixel level.
So yes that could be with a webgl pixel shader, but you'll have to fight just to get the boilerplate running ok on all platform (or get a lib to do that for you).
And anyway there's a solution perfect for your need, and fast enough (a few ms) : use raw pixel data, update them one by one with the relevant function, then draw the result.
The steps to do that are :
- create a buffer same size as the canvas.
- iterate through it's pixel, keeping track of the x,y of the point.
- normalize the coordinates so they match your 'space'.
- compute the value for the normalized (x,y) out of all the data that you have.
- write a color (in my example i choose greyscale) out of that value.
- draw the whole buffer to canvas.
I did a jsfiddle, and here's the result with 4 data points :
fiddle is here :
http://jsfiddle.net/gamealchemist/KsM9c/3/
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
var width = canvas.width,
height = canvas.height;
// builds an image for the target canvas
function buildImage(targetCanvas, valueForXY, someData) {
var width = targetCanvas.width;
var height = targetCanvas.height;
var tempImg = ctx.createImageData(width, height);
var buffer = tempImg.data;
var offset = 0;
var xy = [0, 0];
function normalizeXY(xy) {
xy[0] = xy[0] / width ;
xy[1] = xy[1] / height;
}
for (var y = 0; y < height; y++)
for (var x = 0; x < width; x++, offset += 4) {
xy[0] = x; xy[1]=y;
normalizeXY(xy);
var val = Math.floor(valueForXY(xy, someData) * 255);
buffer[offset] = val;
buffer[offset + 1] = val;
buffer[offset + 2] = val;
buffer[offset + 3] = 255;
}
ctx.putImageData(tempImg, 0, 0);
}
// return normalized (0->1) value for x,y and
// provided data.
// xy is a 2 elements array
function someValueForXY(xy, someData) {
var res = 0;
for (var i = 0; i < someData.length; i++) {
var thisData = someData[i];
var dist = Math.pow(sq(thisData[0] - xy[0]) + sq(thisData[1] - xy[1]), -0.55);
localRes = 0.04 * dist;
res += localRes;
}
if (res > 1) res = 1;
return res;
}
var someData = [
[0.6, 0.2],
[0.35, 0.8],
[0.2, 0.5],
[0.6, 0.75]
];
buildImage(canvas, someValueForXY, someData);
// ------------------------
function sq(x) {
return x * x
}
In fact the GameAlchemist's solution isn't fast or I do something really wrong. I've implemented this algo only for the top view because the front view is much more complex.
For 120 lights the top view take 100-105 ms with the old code and it take 1650-1700 ms with this code (and moreover it still lacks a few things in the new code like the color for example):
drawTopLightForColor_ : function(canvasW, canvasD, rampX, rampY, rampZ, ledsArrays, color) {
function sq(x) {
return x * x;
}
var tmpImg = Distri.topCanvasCtx.createImageData(canvasW, canvasD);
var rawData = tmpImg.data;
var ledsArray = ledsArrays[color];
var len = ledsArray.length;
var i = 0;
for (var y = 0; y < canvasD; y++) {
for (var x = 0; x < canvasW; x++, i += 4) {
var intensity = 0;
for (var j = 0; j < len; j++) {
intensity += 2 * Math.pow(
sq((rampX + ledsArray[j].x) - x) +
sq((rampZ + ledsArray[j].y) - y),
-0.5
);
}
if (intensity > 1) {
intensity = 1;
}
intensity = Math.round(intensity * 255);
rawData[i] = intensity;
rawData[i + 1] = intensity;
rawData[i + 2] = intensity;
rawData[i + 3] = 255;
}
}
Distri.topCanvasCtx.putImageData(tmpImg, 0, 0);
},
Am I doing something wrong?