Skip to content

Instantly share code, notes, and snippets.

@alexbourt
Last active September 8, 2017 22:42
Show Gist options
  • Save alexbourt/ca5b9e44f6ecbfcc6b8a52d805caa9f6 to your computer and use it in GitHub Desktop.
Save alexbourt/ca5b9e44f6ecbfcc6b8a52d805caa9f6 to your computer and use it in GitHub Desktop.

Revisions

  1. alexbourt revised this gist Sep 8, 2017. 1 changed file with 40 additions and 1 deletion.
    41 changes: 40 additions & 1 deletion gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -40,7 +40,46 @@ function createParticles(canvas, width, height, cellSize = defaultCellSize)
    canvas.mouseX = 0;
    canvas.mouseY = 0;

    // cells
    canvas.onclick = function(e)
    {
    if (e.button == 0)
    {
    var canvasRect = canvas.getBoundingClientRect();

    var px = e.clientX - canvasRect.left;
    var py = e.clientY - canvasRect.top;

    var size = 300;
    var gap = 20;

    for (var y = py - size/2; y < py + size/2; y += gap)
    {
    for (var x = px - size/2; x < px + size/2; x += gap)
    {
    if ( x < 0 || x >= canvas.width
    && y < 0 || y >= canvas.height)
    continue;

    addParticle({
    cell: null,
    px: x,
    py: y,
    ppx: x,
    ppy: y,
    fx: 0,
    fy: 0,
    vx: 0,
    vy: 0,
    color: '#ac4',
    density: 0,
    nearDensity: 0
    });
    }
    }
    }
    };

    // cells

    canvas.cells = new Array();

  2. alexbourt revised this gist Sep 8, 2017. 1 changed file with 0 additions and 36 deletions.
    36 changes: 0 additions & 36 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -207,8 +207,6 @@ function createParticles(canvas, width, height, cellSize = defaultCellSize)
    }
    };



    canvas.moveParticles = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    @@ -228,40 +226,6 @@ function createParticles(canvas, width, height, cellSize = defaultCellSize)

    // forces

    canvas.applyRandom = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    p.vx += (-1 + Math.random()*2) * canvas.random;
    p.vy += (-1 + Math.random()*2) * canvas.random;
    }
    }
    };



    canvas.applyDamping = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    p.vx *= 1 - canvas.damping;
    p.vy *= 1 - canvas.damping;
    }
    }
    };

    canvas.applyGravity = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
  3. alexbourt revised this gist Sep 8, 2017. 1 changed file with 0 additions and 341 deletions.
    341 changes: 0 additions & 341 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -40,131 +40,6 @@ function createParticles(canvas, width, height, cellSize = defaultCellSize)
    canvas.mouseX = 0;
    canvas.mouseY = 0;

    canvas.addEventListener("mousedown", canvas.onmousedown, false);
    canvas.addEventListener("mouseup" , canvas.onmouseup , false);
    canvas.addEventListener("mousemove", canvas.onmousemove, false);
    //canvas.addEventListener("touchstart", canvas.ontouchstart, false);
    //canvas.addEventListener("touchend" , canvas.ontouchend , false);
    //canvas.addEventListener("touchmove", canvas.ontouchmove, false);

    canvas.onclick = function(e)
    {
    if (e.button == 0)
    {
    var canvasRect = canvas.getBoundingClientRect();

    var px = e.clientX - canvasRect.left;
    var py = e.clientY - canvasRect.top;

    var size = 300;
    var gap = 20;

    for (var y = py - size/2; y < py + size/2; y += gap)
    {
    for (var x = px - size/2; x < px + size/2; x += gap)
    {
    if ( x < 0 || x >= canvas.width
    && y < 0 || y >= canvas.height)
    continue;

    addParticle({
    cell: null,
    px: x,
    py: y,
    ppx: x,
    ppy: y,
    fx: 0,
    fy: 0,
    vx: 0,
    vy: 0,
    color: '#ac4',
    density: 0,
    nearDensity: 0
    });
    }
    }
    }
    };

    canvas.onmousedown = function(e)
    {
    if (e.button == 0)
    {
    canvas.mouseDown = true;

    var canvasRect = canvas.getBoundingClientRect();

    canvas.mouseX = e.clientX - canvasRect.left;
    canvas.mouseY = e.clientY - canvasRect.top;
    }
    };

    canvas.onmousemove = function(e)
    {
    if (canvas.mouseDown)
    {
    e.stopImmediatePropagation();

    var canvasRect = canvas.getBoundingClientRect();

    var ex = e.clientX - canvasRect.left;
    var ey = e.clientY - canvasRect.top;

    var ix = ex - canvas.mouseX;
    var iy = ey - canvas.mouseY;

    canvas.mouseX = ex;
    canvas.mouseY = ey;

    var h = canvas.cellSize;

    var _cx = Math.round(ex / canvas.cellSize);
    var _cy = Math.round(ey / canvas.cellSize);

    var _ci = _cy * canvas.xCells + _cx;
    var cell = canvas.cells[_ci];

    var cyMin = Math.max(0, cell.cy - 2);
    var cyMax = Math.min(cell.cy + 2, canvas.yCells-1);
    var cxMin = Math.max(0, cell.cx - 2);
    var cxMax = Math.min(cell.cx + 2, canvas.xCells-1);

    for (var cy = cyMin; cy <= cyMax; cy++)
    {
    for (var cx = cxMin; cx <= cxMax; cx++)
    {
    var ci = cy * canvas.xCells + cx;
    var checkCell = canvas.cells[ci];

    for (var k = 0; k < checkCell.particles.length; k++)
    {
    var p = checkCell.particles[k];

    var dx = ex - p.px;
    var dy = ey - p.py;

    var dist2 = dx*dx + dy*dy;

    if (dist2 >= h*h) // check if particle is too far
    continue;

    var dist = Math.sqrt(dist2);
    var q = Math.pow(1 - dist/h, 0.5);

    p.vx += q * ix / 2;
    p.vy += q * iy / 2;
    }
    }
    }
    }
    };

    canvas.onmouseup = function(e)
    {
    if (e.button == 0)
    canvas.mouseDown = false;
    };

    // cells

    canvas.cells = new Array();
    @@ -259,33 +134,6 @@ function createParticles(canvas, width, height, cellSize = defaultCellSize)
    }
    };

    //function updateCellsAt(cxMin, cyMin, cxMax, cyMax)
    //{
    // for (var cy = cyMin; cy <= cyMax; cy++)
    // {
    // for (var cx = cxMin; cx <= cxMax; cx++)
    // {
    // var cell = canvas.cells[cy * canvas.xCells + cx];

    // for (var i = cell.particles.length - 1; i >= 0; i--)
    // {
    // var p = cell.particles[i];

    // if ( p.px >= cell.left
    // && p.py >= cell.top
    // && p.px < cell.right
    // && p.py < cell.bottom)
    // continue;

    // cell.particles.splice(i, 1);
    // p.cell = null;

    // addParticle(p);
    // }
    // }
    // }
    //};

    // particles

    function addParticle(p)
    @@ -305,75 +153,8 @@ function createParticles(canvas, width, height, cellSize = defaultCellSize)
    cell.particles.push(p);
    }

    //function moveParticle(p, x, y)
    //{
    // p.px = x;
    // p.py = y;

    // if ( p.px >= p.cell.left
    // && p.py >= p.cell.top
    // && p.px < p.cell.right
    // && p.py < p.cell.bottom)
    // return;

    // p.cell.particles.splice(
    // p.cell.particles.indexOf(p),
    // 1);
    // p.cell = null;

    // addParticle(p);
    //}

    /////////////////////// create some particles

    var radius = 2;

    //var gap = 10;

    //for (var py = canvas.height/2 + gap; py < canvas.height - radius; py += gap)
    //{
    // for (var px = radius; px < canvas.width - radius; px += gap)
    // {
    // canvas.addParticle({
    // px: px,
    // py: py,
    // ppx: px,
    // ppy: py,
    // dx: 0,
    // dy: 0,
    // vx: 0, //100 * (-1 + 2*Math.random()),
    // vy: 0, //100 * (-1 + 2*Math.random()),
    // radius: radius,
    // color: '#ac4',
    // density: 0,
    // nearDensity: 0
    // //bounce: 1,
    // //colliding: false
    // });
    // }
    //}

    //for (var py = radius; py < canvas.height/2; py += gap)
    //{
    // for (var px = radius; px < canvas.width - radius; px += gap)
    // {
    // canvas.addParticle({
    // px: px,
    // py: py,
    // ppx: px,
    // ppy: py,
    // vx: 0, //100 * (-1 + 2*Math.random()),
    // vy: 0, //100 * (-1 + 2*Math.random()),
    // radius: radius,
    // color: '#0cf',
    // density: 0,
    // nearDensity: 0
    // //bounce: 1,
    // //colliding: false
    // });
    // }
    //}

    // update

    canvas.update = function()
    @@ -800,128 +581,6 @@ function createParticles(canvas, width, height, cellSize = defaultCellSize)
    }
    };

    //canvas.bounceOffOther = function()
    //{
    // // mark all as uncollided

    // for (var i = 0; i < canvas.cells.length; i++)
    // {
    // var cell = canvas.cells[i];

    // for (var j = 0; j < cell.particles.length; j++)
    // cell.particles[j].colliding = false;
    // }

    // // test for collisions
    // // http://www.gamasutra.com/view/feature/131424/pool_hall_lessons_fast_accurate_.php

    // for (var i = 0; i < canvas.cells.length; i++)
    // {
    // var cell = canvas.cells[i];

    // var cyMin = Math.max(0, cell.cy - 1);
    // var cyMax = Math.min(cell.cy + 1, canvas.yCells - 1);
    // var cxMin = Math.max(0, cell.cx - 1);
    // var cxMax = Math.min(cell.cx + 1, canvas.xCells - 1);

    // for (var j = 0; j < cell.particles.length; j++)
    // {
    // var p = cell.particles[j];

    // for (var cy = cyMin; cy <= cyMax; cy++)
    // {
    // for (var cx = cxMin; cx <= cxMax; cx++)
    // {
    // var checkCell = canvas.cells[cy * canvas.xCells + cx];

    // for (var k = 0; k < checkCell.particles.length; k++)
    // {
    // var c = checkCell.particles[k];

    // var sr = p.radius + c.radius; // sum of radii
    // var sr2 = sr*sr;

    // var pMass = p.radius*p.radius;
    // var cMass = c.radius*c.radius;

    // //

    // var dx = c.px - p.px;
    // var dy = c.py - p.py;

    // // check if the test is against itself
    // if (Math.abs(dx) + Math.abs(dy) == 0)
    // continue;

    // var dp2 = dx*dx + dy*dy;

    // // subtract c vector from p vector to turn moving-moving into moving-static
    // var pvx = p.vx - c.vx;
    // var pvy = p.vy - c.vy;

    // // check if there is definitely no collision
    // var dpv2 = pvx*pvx + pvy*pvy;
    // if (dpv2 < dp2 - sr2) // TODO: check the tutorial to see if the -sr2 really works
    // continue;

    // // check if p is not moving toward c
    // if (pvx*dx + pvy*dy <= 0) // dot product
    // continue;

    // // check if p will intersect c on its trajectory
    // var dv = Math.sqrt(dpv2); // this can be calculated once per particle with the new vector
    // var nx = pvx / dv;
    // var ny = pvy / dv;
    // var d2t2 = nx*dx + ny*dy; // distance to closest tangent
    // var d2c2 = dp2 - d2t2; // square of distance from closest tangent to center of c
    // if (d2c2 > sr2)
    // continue;

    // // calculate how far p can move before it hits c
    // var dist = Math.sqrt(d2t2) - Math.sqrt(sr2 - d2c2);

    // // check if movement direction is congruent
    // if (dist > dv)
    // continue;

    // p.colliding = true;

    // // shorten movement vectors

    // var shorten = dist/dv;

    // p.vx *= shorten;
    // p.vy *= shorten;
    // c.vx *= shorten;
    // c.vy *= shorten;

    // // bounce

    // var dp = Math.sqrt(dp2);

    // var ndx = dx / dp;
    // var ndy = dy / dp;

    // var pa = p.vx*ndx + p.vy*ndy;
    // var ca = c.vx*ndx + c.vy*ndy;

    // var opt = 2 * (pa - ca) / (pMass + cMass);

    // var npvx = p.vx - opt * cMass * ndx;
    // var npvy = p.vy - opt * cMass * ndy;
    // var ncvx = c.vx - opt * pMass * ndx;
    // var ncvy = c.vy - opt * pMass * ndy;

    // p.vx = npvx;
    // p.vy = npvy;
    // c.vx = ncvx;
    // c.vy = ncvy;
    // }
    // }
    // }
    // }
    // }
    //};

    canvas.updateParticleVelocities = function()
    {
  4. alexbourt created this gist Sep 8, 2017.
    1,086 changes: 1,086 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1086 @@
    var defaultCellSize = 48 * window.devicePixelRatio;

    function createParticles(canvas, width, height, cellSize = defaultCellSize)
    {
    canvas.width = width;
    canvas.height = height;

    // defaults

    canvas.showCells = false;
    canvas.showVelocity = false;
    canvas.showDensity = false;
    canvas.showForces = true;

    canvas.cellSize = cellSize;
    canvas.xCells = Math.ceil(canvas.width / canvas.cellSize);
    canvas.yCells = Math.ceil(canvas.height / canvas.cellSize);

    canvas.scale = 10;

    //canvas.particleBounce = 1;
    canvas.wallBounce = 0.2;
    canvas.damping = 0;

    canvas.random = 0;
    canvas.gravity = 9.8;

    canvas.viscositySigma = 0; // linear viscosity
    canvas.viscosityBeta = 0.001; // quadratic viscosity

    canvas.restDensity = 10;

    canvas.stiffness = 1.5;
    canvas.nearStiffness = 3;

    canvas.time = performance.now();
    canvas.dt = 1;

    canvas.mouseDown = false;
    canvas.mouseX = 0;
    canvas.mouseY = 0;

    canvas.addEventListener("mousedown", canvas.onmousedown, false);
    canvas.addEventListener("mouseup" , canvas.onmouseup , false);
    canvas.addEventListener("mousemove", canvas.onmousemove, false);
    //canvas.addEventListener("touchstart", canvas.ontouchstart, false);
    //canvas.addEventListener("touchend" , canvas.ontouchend , false);
    //canvas.addEventListener("touchmove", canvas.ontouchmove, false);

    canvas.onclick = function(e)
    {
    if (e.button == 0)
    {
    var canvasRect = canvas.getBoundingClientRect();

    var px = e.clientX - canvasRect.left;
    var py = e.clientY - canvasRect.top;

    var size = 300;
    var gap = 20;

    for (var y = py - size/2; y < py + size/2; y += gap)
    {
    for (var x = px - size/2; x < px + size/2; x += gap)
    {
    if ( x < 0 || x >= canvas.width
    && y < 0 || y >= canvas.height)
    continue;

    addParticle({
    cell: null,
    px: x,
    py: y,
    ppx: x,
    ppy: y,
    fx: 0,
    fy: 0,
    vx: 0,
    vy: 0,
    color: '#ac4',
    density: 0,
    nearDensity: 0
    });
    }
    }
    }
    };

    canvas.onmousedown = function(e)
    {
    if (e.button == 0)
    {
    canvas.mouseDown = true;

    var canvasRect = canvas.getBoundingClientRect();

    canvas.mouseX = e.clientX - canvasRect.left;
    canvas.mouseY = e.clientY - canvasRect.top;
    }
    };

    canvas.onmousemove = function(e)
    {
    if (canvas.mouseDown)
    {
    e.stopImmediatePropagation();

    var canvasRect = canvas.getBoundingClientRect();

    var ex = e.clientX - canvasRect.left;
    var ey = e.clientY - canvasRect.top;

    var ix = ex - canvas.mouseX;
    var iy = ey - canvas.mouseY;

    canvas.mouseX = ex;
    canvas.mouseY = ey;

    var h = canvas.cellSize;

    var _cx = Math.round(ex / canvas.cellSize);
    var _cy = Math.round(ey / canvas.cellSize);

    var _ci = _cy * canvas.xCells + _cx;
    var cell = canvas.cells[_ci];

    var cyMin = Math.max(0, cell.cy - 2);
    var cyMax = Math.min(cell.cy + 2, canvas.yCells-1);
    var cxMin = Math.max(0, cell.cx - 2);
    var cxMax = Math.min(cell.cx + 2, canvas.xCells-1);

    for (var cy = cyMin; cy <= cyMax; cy++)
    {
    for (var cx = cxMin; cx <= cxMax; cx++)
    {
    var ci = cy * canvas.xCells + cx;
    var checkCell = canvas.cells[ci];

    for (var k = 0; k < checkCell.particles.length; k++)
    {
    var p = checkCell.particles[k];

    var dx = ex - p.px;
    var dy = ey - p.py;

    var dist2 = dx*dx + dy*dy;

    if (dist2 >= h*h) // check if particle is too far
    continue;

    var dist = Math.sqrt(dist2);
    var q = Math.pow(1 - dist/h, 0.5);

    p.vx += q * ix / 2;
    p.vy += q * iy / 2;
    }
    }
    }
    }
    };

    canvas.onmouseup = function(e)
    {
    if (e.button == 0)
    canvas.mouseDown = false;
    };

    // cells

    canvas.cells = new Array();

    for (var y = 0; y < canvas.yCells; y++)
    {
    for (var x = 0; x < canvas.xCells; x++)
    {
    canvas.cells.push({
    particles: new Array(),
    cx: x,
    cy: y,
    left: x * canvas.cellSize,
    top: y * canvas.cellSize,
    right: (x+1) * canvas.cellSize,
    bottom: (y+1) * canvas.cellSize,
    color: 'rgb(' + Math.floor(64+192*Math.random()) + ', '
    + Math.floor(64+192*Math.random()) + ', '
    + Math.floor(64+192*Math.random()) + ')'
    });
    }
    }


    function saveCells()
    {
    //canvas.oldCells = canvas.cells;
    canvas.oldCells = new Array();

    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    var oldCell = {
    particles: new Array(),
    cx: cell.cx,
    cy: cell.cy,
    left: cell.left,
    top: cell.top,
    right: cell.right,
    bottom: cell.bottom,
    color: cell.color
    };

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    oldCell.particles.push({
    cell: oldCell,
    px: p.px,
    py: p.py,
    ppx: p.ppx,
    ppy: p.ppy,
    fx: p.fx,
    fy: p.fx,
    vx: p.vx,
    vy: p.vx,
    color: p.color,
    density: p.density,
    nearDensity: p.nearDensity
    });
    }

    canvas.oldCells.push(oldCell);
    }
    }


    function updateCells()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = cell.particles.length - 1; j >= 0; j--)
    {
    var p = cell.particles[j];

    if ( p.px >= cell.left
    && p.py >= cell.top
    && p.px < cell.right
    && p.py < cell.bottom
    || p.px < 0 || p.py < 0)
    continue;

    cell.particles.splice(j, 1);
    p.cell = null;

    addParticle(p);
    }
    }
    };

    //function updateCellsAt(cxMin, cyMin, cxMax, cyMax)
    //{
    // for (var cy = cyMin; cy <= cyMax; cy++)
    // {
    // for (var cx = cxMin; cx <= cxMax; cx++)
    // {
    // var cell = canvas.cells[cy * canvas.xCells + cx];

    // for (var i = cell.particles.length - 1; i >= 0; i--)
    // {
    // var p = cell.particles[i];

    // if ( p.px >= cell.left
    // && p.py >= cell.top
    // && p.px < cell.right
    // && p.py < cell.bottom)
    // continue;

    // cell.particles.splice(i, 1);
    // p.cell = null;

    // addParticle(p);
    // }
    // }
    // }
    //};

    // particles

    function addParticle(p)
    {
    var cx = Math.floor(p.px / canvas.cellSize);
    var cy = Math.floor(p.py / canvas.cellSize);

    if ( cx < 0
    || cy < 0
    || cx >= canvas.xCells
    || cy >= canvas.yCells)
    return;

    var cell = canvas.cells[cy * canvas.xCells + cx];

    p.cell = cell;
    cell.particles.push(p);
    }

    //function moveParticle(p, x, y)
    //{
    // p.px = x;
    // p.py = y;

    // if ( p.px >= p.cell.left
    // && p.py >= p.cell.top
    // && p.px < p.cell.right
    // && p.py < p.cell.bottom)
    // return;

    // p.cell.particles.splice(
    // p.cell.particles.indexOf(p),
    // 1);
    // p.cell = null;

    // addParticle(p);
    //}

    /////////////////////// create some particles

    var radius = 2;

    //var gap = 10;

    //for (var py = canvas.height/2 + gap; py < canvas.height - radius; py += gap)
    //{
    // for (var px = radius; px < canvas.width - radius; px += gap)
    // {
    // canvas.addParticle({
    // px: px,
    // py: py,
    // ppx: px,
    // ppy: py,
    // dx: 0,
    // dy: 0,
    // vx: 0, //100 * (-1 + 2*Math.random()),
    // vy: 0, //100 * (-1 + 2*Math.random()),
    // radius: radius,
    // color: '#ac4',
    // density: 0,
    // nearDensity: 0
    // //bounce: 1,
    // //colliding: false
    // });
    // }
    //}

    //for (var py = radius; py < canvas.height/2; py += gap)
    //{
    // for (var px = radius; px < canvas.width - radius; px += gap)
    // {
    // canvas.addParticle({
    // px: px,
    // py: py,
    // ppx: px,
    // ppy: py,
    // vx: 0, //100 * (-1 + 2*Math.random()),
    // vy: 0, //100 * (-1 + 2*Math.random()),
    // radius: radius,
    // color: '#0cf',
    // density: 0,
    // nearDensity: 0
    // //bounce: 1,
    // //colliding: false
    // });
    // }
    //}

    // update

    canvas.update = function()
    {
    var time = performance.now();
    canvas.dt = (time - canvas.time) / 1000 * canvas.scale; // in seconds
    canvas.time = time;

    canvas.updateParticles();
    canvas.paint();
    };

    canvas.updateParticles = function()
    {
    canvas.applyRandom();
    canvas.applyDamping();
    canvas.applyGravity();

    canvas.applyViscosity();

    canvas.saveParticlePositions();

    canvas.moveParticles();
    //updateCells();

    canvas.relaxParticleDensity();
    //updateCells();

    canvas.bounceOffWalls();
    updateCells(); // housekeeping

    canvas.updateParticleVelocities();
    };

    // move

    canvas.saveParticlePositions = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    p.ppx = p.px;
    p.ppy = p.py;
    }
    }
    };



    canvas.moveParticles = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    p.px += p.vx * canvas.dt,
    p.py += p.vy * canvas.dt;
    }
    }
    };


    // forces

    canvas.applyRandom = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    p.vx += (-1 + Math.random()*2) * canvas.random;
    p.vy += (-1 + Math.random()*2) * canvas.random;
    }
    }
    };



    canvas.applyDamping = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    p.vx *= 1 - canvas.damping;
    p.vy *= 1 - canvas.damping;
    }
    }
    };

    canvas.applyGravity = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    cell.particles[j].vy += canvas.gravity * canvas.dt;
    }
    };

    // viscosity

    canvas.applyViscosity = function()
    {
    var h = canvas.cellSize;

    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    var cyMin = Math.max(0, cell.cy - 1);
    var cyMax = Math.min(cell.cy + 1, canvas.yCells - 1);
    var cxMin = Math.max(0, cell.cx - 1);
    var cxMax = Math.min(cell.cx + 1, canvas.xCells - 1);

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    for (var cy = cyMin; cy <= cyMax; cy++)
    {
    for (var cx = cxMin; cx <= cxMax; cx++)
    {
    var ci = cy * canvas.xCells + cx;
    var checkCell = canvas.cells[ci];

    for (var k = 0; k < checkCell.particles.length; k++)
    {
    if (i == ci && j == k) // check if the test is against itself
    continue;

    var p2 = checkCell.particles[k];

    var dx = p2.px - p.px;
    var dy = p2.py - p.py;

    var dist2 = dx*dx + dy*dy;

    if (dist2 >= h*h) // check if particle is too far
    continue;

    var dist = Math.sqrt(dist2);
    var q = 1 - dist/h;

    var dvx = p.vx - p2.vx;
    var dvy = p.vy - p2.vy;

    var x1 = dx / nozero(dist);
    var y1 = dy / nozero(dist);

    var u = dvx*x1 + dvy*y1; // dot product

    if (u > 0)
    {
    var I =
    canvas.dt
    * q
    * ( canvas.viscositySigma * u
    + canvas.viscosityBeta * u*u);

    var Ix = I * x1;
    var Iy = I * y1;

    p.vx -= Ix/2;
    p.vy -= Iy/2;

    p2.vx += Ix/2;
    p2.vy += Iy/2;
    }
    }
    }
    }
    }
    }
    };

    // double density relaxation

    canvas.relaxParticleDensity = function()
    {
    saveCells();

    //

    var h = canvas.cellSize;

    for (var i = 0; i < canvas.cells.length; i++)
    {
    var oldCell = canvas.oldCells[i];
    var cell = canvas.cells[i];

    var cyMin = Math.max(0, cell.cy - 1);
    var cyMax = Math.min(cell.cy + 1, canvas.yCells - 1);
    var cxMin = Math.max(0, cell.cx - 1);
    var cxMax = Math.min(cell.cx + 1, canvas.xCells - 1);

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    p.density = 0;
    p.nearDensity = 0;

    for (var cy = cyMin; cy <= cyMax; cy++)
    {
    for (var cx = cxMin; cx <= cxMax; cx++)
    {
    var ci = cy * canvas.xCells + cx;

    var checkCell = canvas.cells[ci];

    for (var k = 0; k < checkCell.particles.length; k++)
    {
    if (i == ci && j == k) // check if the test is against itself
    continue;

    var p2 = checkCell.particles[k];

    var dx = p2.px - p.px;
    var dy = p2.py - p.py;

    var dist2 = dx*dx + dy*dy;

    if (dist2 >= h*h) // check if particle is too far
    continue;

    var dist = Math.sqrt(dist2);
    var q = 1 - dist/h;

    p.density += Math.sqr (q);
    p.nearDensity += Math.cube(q);
    }
    }
    }

    var pressure = canvas.stiffness * (p.density - canvas.restDensity);
    var nearPressure = canvas.nearStiffness * p.nearDensity;

    p.fx = 0;
    p.fy = 0;

    var pOld = oldCell.particles[j];

    for (var cy = cyMin; cy <= cyMax; cy++)
    {
    for (var cx = cxMin; cx <= cxMax; cx++)
    {
    var ci = cy * canvas.xCells + cx;

    var checkOldCell = canvas.oldCells[ci];
    var checkCell = canvas.cells[ci];

    for (var k = 0; k < checkOldCell.particles.length; k++)
    {
    if (i == ci && j == k) // check if the test is against itself
    continue;

    var p2 = checkCell.particles[k];
    var p2old = checkOldCell.particles[k];

    var dx = p2old.px - pOld.px;
    var dy = p2old.py - pOld.py;

    var dist2 = dx*dx + dy*dy;

    if (dist2 >= h*h) // check if particle is too far
    continue;

    var dist = Math.sqrt(dist2);
    var q = 1 - dist/h;

    var D =
    Math.sqr(canvas.dt)
    * ( pressure * q
    + nearPressure * q*q);

    p2old.fx = D/2 * dx / nozero(dist); // multiplied by unit vector
    p2old.fy = D/2 * dy / nozero(dist);

    p2.px += p2old.fx;
    p2.py += p2old.fy;

    p.fx -= p2old.fx;
    p.fy -= p2old.fy;
    }
    }
    }

    p.px += p.fx;
    p.py += p.fy;
    }
    }
    };

    // collisions

    function dist(x1, y1, x2, y2)
    {
    var dx = x2 - x1;
    var dy = y2 - y1;

    return Math.sqrt(dx*dx + dy*dy);
    }

    function dist2(x1, y1, x2, y2)
    {
    var dx = x2 - x1;
    var dy = y2 - y1;

    return dx*dx + dy*dy;
    }

    canvas.bounceOffWalls = function()
    {
    var left = 0;
    var top = 0;
    var right = canvas.width;
    var bottom = canvas.height;

    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    if (p.px < left)
    {
    var iy = p.ppy + (p.py - p.ppy) * (left - p.ppx) / nozero(p.px - p.ppx);
    var ix = left;

    var d = dist(ix, iy, p.px, p.py);
    var v = dist(p.ppx, p.ppy, p.px, p.py);

    var f = canvas.wallBounce * d / nozero(v);

    p.px = 2 * left - p.px;
    p.ppx = 2 * left - p.ppx;

    p.px = ix + (p.px - ix) * f;
    p.py = iy + (p.py - iy) * f;

    p.ppx = ix + (p.ppx - ix) * f;
    p.ppy = iy + (p.ppy - iy) * f;
    }
    else if (p.px > right)
    {
    var iy = p.ppy + (p.py - p.ppy) * (right - p.ppx) / nozero(p.px - p.ppx);
    var ix = right;

    var d = dist(ix, iy, p.px, p.py);
    var v = dist(p.ppx, p.ppy, p.px, p.py);

    var f = canvas.wallBounce * d / nozero(v);

    p.px = 2 * right - p.px;
    p.ppx = 2 * right - p.ppx;

    p.px = ix + (p.px - ix) * f;
    p.py = iy + (p.py - iy) * f;

    p.ppx = ix + (p.ppx - ix) * f;
    p.ppy = iy + (p.ppy - iy) * f;
    }

    if (p.py < top)
    {
    var ix = p.ppx + (p.px - p.ppx) * (top - p.ppy) / nozero(p.py - p.ppy);
    var iy = top;

    var d = dist(ix, iy, p.px, p.py);
    var v = dist(p.ppx, p.ppy, p.px, p.py);

    var f = canvas.wallBounce * d / nozero(v);

    p.py = 2 * top - p.py;
    p.ppy = 2 * top - p.ppy;

    p.px = ix + (p.px - ix) * f;
    p.py = iy + (p.py - iy) * f;

    p.ppx = ix + (p.ppx - ix) * f;
    p.ppy = iy + (p.ppy - iy) * f;
    }
    else if (p.py > bottom)
    {
    var ix = p.ppx + (p.px - p.ppx) * (bottom - p.ppy) / nozero(p.py - p.ppy);
    var iy = bottom;

    var d = dist(ix, iy, p.px, p.py);
    var v = dist(p.ppx, p.ppy, p.px, p.py);

    var f = canvas.wallBounce * d / nozero(v);

    p.py = 2 * bottom - p.py;
    p.ppy = 2 * bottom - p.ppy;

    p.px = ix + (p.px - ix) * f;
    p.py = iy + (p.py - iy) * f;

    p.ppx = ix + (p.ppx - ix) * f;
    p.ppy = iy + (p.ppy - iy) * f;
    }
    }
    }
    };

    //canvas.bounceOffOther = function()
    //{
    // // mark all as uncollided

    // for (var i = 0; i < canvas.cells.length; i++)
    // {
    // var cell = canvas.cells[i];

    // for (var j = 0; j < cell.particles.length; j++)
    // cell.particles[j].colliding = false;
    // }

    // // test for collisions
    // // http://www.gamasutra.com/view/feature/131424/pool_hall_lessons_fast_accurate_.php

    // for (var i = 0; i < canvas.cells.length; i++)
    // {
    // var cell = canvas.cells[i];

    // var cyMin = Math.max(0, cell.cy - 1);
    // var cyMax = Math.min(cell.cy + 1, canvas.yCells - 1);
    // var cxMin = Math.max(0, cell.cx - 1);
    // var cxMax = Math.min(cell.cx + 1, canvas.xCells - 1);

    // for (var j = 0; j < cell.particles.length; j++)
    // {
    // var p = cell.particles[j];

    // for (var cy = cyMin; cy <= cyMax; cy++)
    // {
    // for (var cx = cxMin; cx <= cxMax; cx++)
    // {
    // var checkCell = canvas.cells[cy * canvas.xCells + cx];

    // for (var k = 0; k < checkCell.particles.length; k++)
    // {
    // var c = checkCell.particles[k];

    // var sr = p.radius + c.radius; // sum of radii
    // var sr2 = sr*sr;

    // var pMass = p.radius*p.radius;
    // var cMass = c.radius*c.radius;

    // //

    // var dx = c.px - p.px;
    // var dy = c.py - p.py;

    // // check if the test is against itself
    // if (Math.abs(dx) + Math.abs(dy) == 0)
    // continue;

    // var dp2 = dx*dx + dy*dy;

    // // subtract c vector from p vector to turn moving-moving into moving-static
    // var pvx = p.vx - c.vx;
    // var pvy = p.vy - c.vy;

    // // check if there is definitely no collision
    // var dpv2 = pvx*pvx + pvy*pvy;
    // if (dpv2 < dp2 - sr2) // TODO: check the tutorial to see if the -sr2 really works
    // continue;

    // // check if p is not moving toward c
    // if (pvx*dx + pvy*dy <= 0) // dot product
    // continue;

    // // check if p will intersect c on its trajectory
    // var dv = Math.sqrt(dpv2); // this can be calculated once per particle with the new vector
    // var nx = pvx / dv;
    // var ny = pvy / dv;
    // var d2t2 = nx*dx + ny*dy; // distance to closest tangent
    // var d2c2 = dp2 - d2t2; // square of distance from closest tangent to center of c
    // if (d2c2 > sr2)
    // continue;

    // // calculate how far p can move before it hits c
    // var dist = Math.sqrt(d2t2) - Math.sqrt(sr2 - d2c2);

    // // check if movement direction is congruent
    // if (dist > dv)
    // continue;

    // p.colliding = true;

    // // shorten movement vectors

    // var shorten = dist/dv;

    // p.vx *= shorten;
    // p.vy *= shorten;
    // c.vx *= shorten;
    // c.vy *= shorten;

    // // bounce

    // var dp = Math.sqrt(dp2);

    // var ndx = dx / dp;
    // var ndy = dy / dp;

    // var pa = p.vx*ndx + p.vy*ndy;
    // var ca = c.vx*ndx + c.vy*ndy;

    // var opt = 2 * (pa - ca) / (pMass + cMass);

    // var npvx = p.vx - opt * cMass * ndx;
    // var npvy = p.vy - opt * cMass * ndy;
    // var ncvx = c.vx - opt * pMass * ndx;
    // var ncvy = c.vy - opt * pMass * ndy;

    // p.vx = npvx;
    // p.vy = npvy;
    // c.vx = ncvx;
    // c.vy = ncvy;
    // }
    // }
    // }
    // }
    // }
    //};

    canvas.updateParticleVelocities = function()
    {
    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    p.vx = (p.px - p.ppx) / nozero(canvas.dt);
    p.vy = (p.py - p.ppy) / nozero(canvas.dt);
    }
    }
    };

    // paint

    canvas.paint = function()
    {
    c = canvas.getContext('2d');

    // background

    c.fillStyle = '#000411';
    c.fillRect(0, 0, canvas.width, canvas.height);

    // particles

    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    for (var j = 0; j < cell.particles.length; j++)
    {
    var p = cell.particles[j];

    // body

    if ( !canvas.showVelocity
    && !canvas.showDensity
    && !canvas.showForces)
    {
    c.beginPath();
    c.arc(p.px, p.py, radius, 0, Tau, false);
    c.fillStyle = canvas.showCells ? cell.color : p.color;

    //if (p.colliding && canvas.showCollisions)
    // c.fillStyle = '#ff4';

    c.fill();
    }

    if (canvas.showVelocity)
    {
    var angle = getAngle(
    p.px,
    p.py,
    p.px + p.vx,
    p.py + p.vy);

    var l = Math.sqrt(p.vx*p.vx + p.vy*p.vy);

    var df = 60;
    l = Math.pow(l / df, 0.75) * df;

    c.beginPath();
    c.moveTo(
    p.px + l * Math.cos(angle) / 2,
    p.py + l * Math.sin(angle) / 2);
    c.lineTo(
    p.px - l * Math.cos(angle) / 2,
    p.py - l * Math.sin(angle) / 2);
    c.lineWidth = 1;
    c.strokeStyle = '#0f0';
    c.stroke();
    }

    else if (canvas.showDensity)
    {
    var h = canvas.cellSize;

    c.beginPath();
    c.arc(p.px, p.py, h/20 * p.density / canvas.restDensity, 0, Tau, false);
    c.lineWidth = 1;//Math.cube(p.density / canvas.restDensity) * 2;
    c.strokeStyle = '#f44';
    c.stroke();
    }

    else if (canvas.showForces)
    {
    var angle = getAngle(
    p.px,
    p.py,
    p.px + p.fx,
    p.py + p.fy);

    var l = dist(0, 0, p.fx, p.fy);

    //var df = 60;
    //l = Math.pow(l / df, 0.75) * df;
    l *= 2 * canvas.scale;

    c.beginPath();
    c.moveTo(
    p.px + l * Math.cos(angle),
    p.py + l * Math.sin(angle));
    c.lineTo(
    p.px - l * Math.cos(angle),
    p.py - l * Math.sin(angle));
    c.lineWidth = 1;

    var scale = 10;
    var pressure = p.density - canvas.restDensity;

    //c.strokeStyle = 'rgb('
    // + Math.min(Math.round(pressure / canvas.restDensity * 255 * scale), 255).toString()
    // + ', 0, '
    // + Math.min(Math.round(Math.abs(-pressure / canvas.restDensity) * 255 * scale), 255).toString()
    // + ')';

    c.strokeStyle = '#ff0';
    c.stroke();
    }
    }
    }

    // cells

    if (canvas.showCells)
    {
    c.lineWidth = 1;

    //c.font = '16px Arial bold, sans-serif';
    //c.textAlign = 'left';
    //c.textBaseline = 'top';

    for (var i = 0; i < canvas.cells.length; i++)
    {
    var cell = canvas.cells[i];

    if (cell.particles.length > 0)
    {
    c.strokeStyle = cell.color;
    c.fillStyle = cell.color;

    c.strokeRect(
    0.5 + cell.left,
    0.5 + cell.top,
    cell.right - cell.left - 1,
    cell.bottom - cell.top - 1);

    //c.fillText(
    // cell.particles.length,
    // cell.left + 2,
    // cell.top + 2);
    }
    }
    }
    };
    }