2d metaballs w javascript

W artykule stworzymy w html5 canvas efekt metaballs. Podstawową koncepcją jest wykorzystanie gradientu radialnego i ustawienie progu alfa, który będzie filtrowany. Na przykład każdy piksel dla alfa niższej niż 150 zostanie ustawiony na alfa 0. Daje to efekt metaballi, ponieważ podczas gdy gradienty pokrywają obszary, które są normalnie ukryte mają obecnie ich wartości alfa dodane co pozwala im znaleźć się powyżej progu.

Kod html musi zawierać tylko element canvas.
<html>
<head>
</head>
<body style="background:black;">
<canvas id="canvas"></canvas>
</body>
</html>
Na początku stórzmy tablicę punktów zawierającą ich położenie, prędkości i wielkości.
for(var i = 0; i < 50; i++){
    var x = Math.random()*width,
	y = Math.random()*height,
	vx = (Math.random()*8)-4,
	vy = (Math.random()*8)-4,
	size = Math.floor(Math.random()*60)+60;
    
    points.push({x:x,y:y,vx:vx,vy:vy, size:size});
};
Następnie rysujemy punkty na tymczasowym canvasie.
var grad = tempCtx.createRadialGradient(point.x, point.y, 1, point.x, point.y, point.size);

tempCtx.beginPath();
grad.addColorStop(0, 'rgba(' + colors.r +',' + colors.g + ',' + colors.b + ',1)');
grad.addColorStop(1, 'rgba(' + colors.r +',' + colors.g + ',' + colors.b + ',0)');
tempCtx.fillStyle = grad;
tempCtx.arc(point.x, point.y, point.size, 0, Math.PI*2);
tempCtx.fill();
Na koniec czytamy dane pikseli, filtrujemy alfa w oparciu o próg i umieszczamy dane z powrotem w canvasie.
function metaballs(){
    var imageData = tempCtx.getImageData(0,0,width,height),
        pix = imageData.data;
    
    for (var i = 0, n = pix.length; i <n; i += 4) {
        // Checks threshold
        if(pix[i+3]<threshold){
            pix[i+3]=0;
        }
    }
    ctx.putImageData(imageData, 0, 0);    
}
Pełny kod javascript:
var canvas = document.getElementById("canvas"),
    ctx = canvas.getContext("2d"),
    tempCanvas = document.createElement("canvas"),
    tempCtx = tempCanvas.getContext("2d"),
    width = 512,
    height = 512,
    threshold = 210,
    colors = {r:255,g:0,b:0}, cycle = 0,
    points = [];

canvas.width = tempCanvas.width = width;
canvas.height= tempCanvas.height= height;

for(var i = 0; i < 50; i++){
    var x = Math.random()*width,
        y = Math.random()*height,
        vx = (Math.random()*8)-4,
        vy = (Math.random()*8)-4,
        size = Math.floor(Math.random()*60)+60;
    
    points.push({x:x,y:y,vx:vx,vy:vy, size:size});
                       
};

function update(){
    var len = points.length;
    tempCtx.clearRect(0,0,width,height);
    while(len--){
        var point = points[len];
        point.x+=point.vx;
        point.y+=point.vy;
        
        if(point.x > width+point.size){
            point.x = 0-point.size;
        }
        if(point.x < 0-point.size){
            point.x = width+point.size;
        }
        if(point.y > height+point.size){
            point.y = 0-point.size;
        }
        if(point.y < 0-point.size){
            point.y = height+point.size;
       }
        
        tempCtx.beginPath();
        var grad = tempCtx.createRadialGradient(point.x, point.y, 1, point.x, point.y, point.size);
        grad.addColorStop(0, 'rgba(' + colors.r +',' + colors.g + ',' + colors.b + ',1)');
        grad.addColorStop(1, 'rgba(' + colors.r +',' + colors.g + ',' + colors.b + ',0)');
        tempCtx.fillStyle = grad;
        tempCtx.arc(point.x, point.y, point.size, 0, Math.PI*2);
        tempCtx.fill();
    }
    metaballs();
    colorCycle();
    setTimeout(update,10);
}

function colorCycle(){
    cycle+=0.1;
    if(cycle>100){
        cycle = 0;   
    }
    colors.r = ~~(Math.sin(.3*cycle + 0) * 127+ 128);
    colors.g =  ~~(Math.sin(.3*cycle + 2) * 127+ 128);
    colors.b = ~~( Math.sin(.3*cycle + 4) * 127+ 128);
}

function metaballs(){
    var imageData = tempCtx.getImageData(0,0,width,height),
        pix = imageData.data;
    
    for (var i = 0, n = pix.length; i <n; i += 4) {
        if(pix[i+3]<threshold){
           pix[i+3]/=6;
            if(pix[i+3]>threshold/4){
                pix[i+3]=0;
            }
        }
    }
    ctx.putImageData(imageData, 0, 0);    
}

update();