├── README.md
├── circle.png
├── index.html
└── js
├── Grid.js
├── Neighbor.js
├── Particle.js
├── SPH.js
├── index.js
└── lib
└── hand.minified.js
/README.md:
--------------------------------------------------------------------------------
1 | SPHjs - Fluid/Water simulation is JavaScript.
2 | =============================================
3 |
4 | ## Demo
5 | Check the live version here: http://jsexperiments.herokuapp.com/sph/
6 |
7 |
8 | ## Getting started
9 | * Clone or Download the repo.
10 | * Run the index.html through any webserver. Due to browser's security problems.
11 | * Modify, as you please. Do tell me what you come up with :)
12 | * If you can make it more efficient and fast, I would be happy to add your code here.
13 |
14 |
15 |
16 | ## MIT Licence
17 |
18 |
19 | Permission is hereby granted, free of charge, to any person
20 | obtaining a copy of this software and associated documentation
21 | files (the "Software"), to deal in the Software without restriction,
22 | including without limitation the rights to use, copy, modify,
23 | merge, publish, distribute, sublicense, and/or sell copies of
24 | the Software, and to permit persons to whom the Software is
25 | furnished to do so, subject to the following conditions:
26 |
27 | The above copyright notice and this permission notice shall be included
28 | in all copies or substantial portions of the Software.
29 |
30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
31 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
32 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
33 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
34 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
35 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 |
--------------------------------------------------------------------------------
/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asadm/SPHjs/5bdbc544a0317f4fe748eec5480476b6178bb767/circle.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SPH
6 |
7 |
8 |
9 |
10 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | SPH Fluid
32 | This is an unpolished version. Click to add particles. Right-Click on particles to simulate touch.
AsadMemon
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/js/Grid.js:
--------------------------------------------------------------------------------
1 | var Grid = function() {
2 | this.particles = [];
3 | this.numParticles = 0;
4 | };
5 |
6 | Grid.prototype = {
7 | clear: function () {
8 | this.numParticles = 0;
9 | },
10 | add: function(p) {
11 | this.particles[this.numParticles++] = p;
12 | }
13 | };
--------------------------------------------------------------------------------
/js/Neighbor.js:
--------------------------------------------------------------------------------
1 | var Neighbor = function (SPH) {
2 | this.SPH = SPH;
3 | this.p1 = null;
4 | this.p2 = null;
5 | this.distance = 0;
6 | this.nx = 0;
7 | this.ny = 0;
8 | this.weight = 0;
9 | };
10 | Neighbor.prototype = {
11 | setParticle: function (p1, p2) {
12 | this.p1 = p1;
13 | this.p2 = p2;
14 | this.nx = p1.x - p2.x;
15 | this.ny = p1.y - p2.y;
16 | this.distance = Math.sqrt(this.nx * this.nx + this.ny * this.ny);
17 | this.weight = 1 - this.distance / this.SPH.RANGE;
18 | var temp = this.weight * this.weight * this.weight;
19 | p1.density += temp;
20 | p2.density += temp;
21 | temp = 1 / this.distance;
22 | this.nx *= temp;
23 | this.ny *= temp;
24 | },
25 | calcForce: function () {
26 | var p1 = this.p1;
27 | var p2 = this.p2;
28 | var pressureWeight = this.weight * (p1.pressure + p2.pressure) / (p1.density + p2.density) * this.SPH.PRESSURE;
29 | var viscosityWeight = this.weight / (p1.density + p2.density) * this.SPH.VISCOSITY ;
30 | p1.fx += this.nx * pressureWeight;
31 | p1.fy += this.ny * pressureWeight;
32 | p2.fx -= this.nx * pressureWeight;
33 | p2.fy -= this.ny * pressureWeight;
34 | var rvx = p2.vx - p1.vx;
35 | var rvy = p2.vy - p1.vy;
36 | p1.fx += rvx * viscosityWeight;
37 | p1.fy += rvy * viscosityWeight;
38 | p2.fx -= rvx * viscosityWeight;
39 | p2.fy -= rvy * viscosityWeight;
40 | }
41 | };
--------------------------------------------------------------------------------
/js/Particle.js:
--------------------------------------------------------------------------------
1 |
2 | var Particle = function(x, y, SPH) {
3 | this.SPH = SPH;
4 | this.x = x;
5 | this.y = y;
6 | this.gx = 0;
7 | this.gy = 0;
8 | this.vx = 0;
9 | this.vy = 0;
10 | this.fx = 0;
11 | this.fy = 0;
12 | this.density = 0;
13 | this.pressure = 0;
14 | };
15 | Particle.prototype = {
16 | move: function(tdelta) {
17 | var time = tdelta/0.064;
18 |
19 | //touch checks
20 |
21 | for (var i in this.SPH.touches)
22 | {
23 | var t = this.SPH.touches[i];
24 | if (t.down)
25 | {
26 | var dist = Math.abs(this.x-t.x)*Math.abs(this.x-t.x) +
27 | Math.abs(this.y-t.y)*Math.abs(this.y-t.y);
28 |
29 | if (dist0 && fx<=1)
40 | {
41 |
42 | fx *= (dx<0)?-1:1;
43 |
44 | this.vx = (0.8*fx*5) + this.vx*0.3;
45 |
46 | }
47 |
48 | if (fy>0 && fy<=1)
49 | {
50 | fy *= (dy<0)?-1:1;
51 | this.vy = (0.5*fy*5) + this.vy*0.5;
52 |
53 | }
54 |
55 | }
56 |
57 | }
58 |
59 | }
60 |
61 | this.vy += this.SPH.GRAVITYY;
62 | this.vx += this.SPH.GRAVITYX;
63 | this.vx += this.fx;
64 | this.vy += this.fy;
65 | this.x += this.vx *time;
66 | this.y += this.vy *time;
67 | if (this.x < 5)
68 | this.vx += (5 - this.x) * 0.5 - this.vx * 0.5;
69 | if (this.y < 5)
70 | this.vy += (5 - this.y) * 0.5 - this.vy * 0.5;
71 | if (this.x > this.SPH.CanvasWidth)
72 | this.vx += (this.SPH.CanvasWidth - this.x) * 0.5 - this.vx * 0.5;
73 | if (this.y > this.SPH.CanvasHeight)
74 | this.vy += (this.SPH.CanvasHeight - this.y) * 0.5 - this.vy * 0.5;
75 |
76 | }
77 | };
--------------------------------------------------------------------------------
/js/SPH.js:
--------------------------------------------------------------------------------
1 | var SPH = function(canvas){
2 | this.GRAVITYX= 0;
3 | this.GRAVITYY= 0.5;
4 | this.RANGE= 50;
5 | this.RANGE2= this.RANGE * this.RANGE; //range square
6 | this.PRESSURE= 0.25;
7 | this.VISCOSITY= 0.16;
8 | this.PARTICLESIZE=40;
9 | this.DENSITY= 0.2;
10 | this.NUM_GRIDS= 24;
11 | this.CanvasWidth= canvas.width;
12 | this.CanvasHeight= canvas.height;
13 | this.INV_GRID_SIZE= 1 / (this.CanvasHeight / this.NUM_GRIDS);
14 |
15 | this.mouseX=0;
16 | this.mouseY=0;
17 |
18 | this.canvas = canvas;
19 | this.ctx = canvas.getContext('2d');
20 |
21 |
22 | this.particles = [];
23 | this.numParticles = 0;
24 | this.neighbors = [];
25 | this.numNeighbors = 0;
26 | this.count = 0;
27 | this.press = false;
28 | this.grids = [];
29 | this.first = 0;
30 |
31 | this.threshold = 160;
32 | this.colors = { r: 135, g: 206, b: 235 };
33 | this.cycle = 0;
34 |
35 | this.img = new Image();
36 | this.img.src = 'circle.png';
37 |
38 |
39 | for (var i = 0; i < this.NUM_GRIDS; i++) {
40 | this.grids[i] = new Array(this.NUM_GRIDS);
41 | for (var j = 0; j < this.NUM_GRIDS; j++)
42 | this.grids[i][j] = new Grid();
43 | }
44 |
45 | this.touches = [{x:140,y:00,down:false}]; //needs obj type of {x:10,y:10,down:true}
46 | this.touchradius = 1000;
47 | this.touchradius2 = this.touchradius*this.touchradius;
48 |
49 |
50 | }
51 |
52 |
53 | //add more water at mouse position
54 | SPH.prototype.pour = function() {
55 | //if (1)
56 | for (var i = -4; i <= 4; i++) {
57 | var p = new Particle(this.mouseX + i * 8, this.mouseY, this);
58 | p.vy = 3;
59 | this.particles[this.numParticles++] = p;
60 | //alert(mouseX);
61 | }
62 | }
63 |
64 | //calculations for SPH
65 | SPH.prototype.move = function() {
66 |
67 |
68 | this.count++;
69 | //var i;
70 | //var p;
71 | this.updateGrids();
72 | this.findNeighbors();
73 | this.calcPressure();
74 | this.calcForce();
75 | for (var i = 0; i < this.numParticles; i++) {
76 | var p = this.particles[i];
77 | p.move(0.06);
78 | }
79 | }
80 |
81 | //calculate p.gx (position on mini grid) from p.x(position on canvas)
82 | SPH.prototype.updateGrids = function() {
83 | //clear grid
84 | var i;
85 | var j;
86 | for (i = 0; i < this.NUM_GRIDS; i++)
87 | for (j = 0; j < this.NUM_GRIDS; j++)
88 | this.grids[i][j].clear();
89 |
90 |
91 | for (i = 0; i < this.numParticles; i++) {
92 | var p = this.particles[i];
93 | p.fx = p.fy = p.density = 0;
94 | p.gx = Math.floor(p.x * this.INV_GRID_SIZE); //reduce x to grid/canvas scale ratio to find gx
95 | p.gy = Math.floor(p.y * this.INV_GRID_SIZE);
96 |
97 | //check limits
98 | if (p.gx < 0)
99 | p.gx = 0;
100 | if (p.gy < 0)
101 | p.gy = 0;
102 | if (p.gx > this.NUM_GRIDS - 1)
103 | p.gx = this.NUM_GRIDS - 1;
104 | if (p.gy > this.NUM_GRIDS - 1)
105 | p.gy = this.NUM_GRIDS - 1;
106 |
107 | //add particle to grids
108 | this.grids[p.gx][p.gy].add(p);
109 | }
110 | }
111 |
112 |
113 | SPH.prototype.findNeighbors = function() {
114 | this.numNeighbors = 0;
115 | for (var i = 0; i < this.numParticles; i++) {
116 | var p = this.particles[i];
117 |
118 | this.findNeighborsInGrid(p, this.grids[p.gx][p.gy]);
119 | }
120 | }
121 |
122 | SPH.prototype.findNeighborsInGrid = function(pi, g) {
123 | for (var j = 0; j < g.numParticles; j++) {
124 | var pj = g.particles[j];
125 | if (pi == pj)
126 | continue;
127 | var distance = Math.abs(pi.x-pj.x) + Math.abs(pi.y-pj.y);//(pi.x - pj.x) * (pi.x - pj.x) + (pi.y - pj.y) * (pi.y - pj.y);
128 | if (distance < this.RANGE) {
129 | if (this.neighbors.length == this.numNeighbors)
130 | this.neighbors[this.numNeighbors] = new Neighbor(this);
131 | this.neighbors[this.numNeighbors++].setParticle(pi, pj);
132 | }
133 | }
134 | }
135 | SPH.prototype.calcPressure = function() {
136 | for (var i = 0; i < this.numParticles; i++) {
137 | var p = this.particles[i];
138 | if (p.density < this.DENSITY)
139 | p.density = this.DENSITY;
140 | p.pressure = p.density - this.DENSITY;
141 | }
142 | }
143 | SPH.prototype.calcForce = function() {
144 | for (var i = 0; i < this.numNeighbors; i++) {
145 | var n = this.neighbors[i];
146 | n.calcForce();
147 | }
148 | }
--------------------------------------------------------------------------------
/js/index.js:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | var initialize = function(){
5 |
6 | //var canvas = document.getElementById('canvas');
7 | var canvas;
8 | if (navigator.isCocoonJS)
9 | {
10 | canvas= document.createElement('screencanvas');
11 |
12 | }
13 | else
14 | {
15 | canvas = document.createElement('canvas');
16 | }
17 |
18 | canvas.width = window.innerWidth/2 ;
19 | canvas.height = window.innerHeight/2 ;
20 | document.body.appendChild(canvas);
21 |
22 |
23 | console.log("canvas added");
24 |
25 | var ctx = canvas.getContext('2d');
26 |
27 | ctx.fillStyle = '#DDDDDD';
28 | ctx.fillRect(0,0,canvas.width, canvas.height);
29 |
30 |
31 | //Meta ball variables
32 | tempcanvas = document.createElement("canvas");
33 | tempcanvas.width = canvas.width;
34 | tempcanvas.height = canvas.height;
35 | tempctx = tempcanvas.getContext("2d");
36 |
37 | var Water = new SPH(canvas);
38 | var press=0;
39 |
40 |
41 | //LOOP
42 | function frame(e) {
43 |
44 | if (press)
45 | Water.pour();
46 | Water.move();
47 | draw();
48 | window.requestAnimationFrame(frame);
49 | // window.setTimeout(frame, 1000/60);
50 |
51 | }
52 |
53 |
54 | //clear the canvas, draw on tempcanvas, apply threshold, draw on actual canvas and draw lines (obstacles)
55 | function draw() {
56 | //ctx.clearRect(0, 0, SPH.CanvasWidth, SPH.CanvasHeight);
57 | //ctx.fillStyle = '#DDDDDD';
58 | //ctx.fillRect(0,0,canvas.width,canvas.height);
59 |
60 |
61 | tempctx.clearRect(0, 0, Water.CanvasWidth, Water.CanvasHeight);
62 | //tempctx.fillStyle = '#DDDDDD';
63 | //tempctx.fillRect(0,0,canvas.width,canvas.height);
64 | for (var i = 0; i < Water.numParticles; i++) {
65 | var p = Water.particles[i];
66 |
67 | tempctx.drawImage(Water.img, p.x, p.y, Water.PARTICLESIZE, Water.PARTICLESIZE);
68 |
69 | }
70 |
71 |
72 | //metaballize
73 |
74 | var imageData = tempctx.getImageData(0, 0, Water.CanvasWidth, Water.CanvasHeight);
75 |
76 |
77 | pix = imageData.data;
78 |
79 | for (var i = 0, n = pix.length; i < n; i += 4) {
80 | if (pix[i + 3] < Water.threshold) { //threshold and remove blobs with less alpha than thresh.
81 | pix[i + 3] = 0;
82 | }
83 | }
84 | /**/
85 | ctx.putImageData(imageData, 0, 0);
86 |
87 | }
88 |
89 |
90 |
91 |
92 | //window.addEventListener('mousedown', function(e) {mouseX = e.layerX; mouseY = e.layerY;press = true;}, false);
93 | //window.addEventListener('mousemove', function (e) { mouseX = e.layerX; mouseY = e.layerY; document.title = "Particles: " + numParticles+ " Mouse Coords:"+mouseX + "," + mouseY; }, false);
94 | //window.addEventListener('mouseup', function(e) {press = false;}, false);
95 | var c = canvas;
96 | c.addEventListener('pointerdown', function(e)
97 | {console.log(e);
98 | Water.mouseX = e.layerX; Water.mouseY = e.layerY;
99 | if (e.button==0) press = true;
100 | else Water.touches[0].down = true;
101 | }, false);
102 | c.addEventListener('pointermove', function (e)
103 | { Water.mouseX = e.layerX; Water.mouseY = e.layerY;
104 | Water.touches[0].x = e.layerX;Water.touches[0].y = e.layerY;
105 |
106 | document.title = "Particles: " + Water.numParticles+ " Mouse Coords:"+Water.mouseX + "," + Water.mouseY;
107 | }, false);
108 |
109 |
110 | c.addEventListener('pointerup', function(e) {press = false;Water.touches[0].down=false;}, false);
111 |
112 | //touch specific
113 | c.addEventListener('touchstart', function(touchEvent) {var e= touchEvent.targetTouches[0]; Water.mouseX = e.pageX; Water.mouseY = e.pageY;press = true;}, false);
114 | c.addEventListener('touchmove', function (touchEvent) { var e= touchEvent.targetTouches[0]; Water.mouseX = e.pageX; Water.mouseY = e.pageY; console.log( "Particles: " + Water.numParticles+ " Mouse Coords:"+Water.mouseX + "," + Water.mouseY); }, false);
115 | c.addEventListener('touchend', function(touchEvent) {press = false;}, false);
116 | frame();
117 |
118 |
119 |
120 | };
121 |
122 |
123 |
124 | document.body.onload = initialize();
125 |
--------------------------------------------------------------------------------
/js/lib/hand.minified.js:
--------------------------------------------------------------------------------
1 | (function(){var e=["PointerDown","PointerUp","PointerMove","PointerOver","PointerOut","PointerCancel","PointerEnter","PointerLeave","pointerdown","pointerup","pointermove","pointerover","pointerout","pointercancel","pointerenter","pointerleave"];var t="touch";var n="pen";var r="mouse";var i=function(e,i){var s=document.createEvent("MouseEvents");s.initMouseEvent(i,true,true,window,1,e.screenX,e.screenY,e.clientX,e.clientY,e.ctrlKey,e.altKey,e.shiftKey,e.metaKey,e.button,null);if(s.offsetX===undefined){if(e.offsetX!==undefined){if(Object&&Object.defineProperty!==undefined){Object.defineProperty(s,"offsetX",{writable:true});Object.defineProperty(s,"offsetY",{writable:true})}s.offsetX=e.offsetX;s.offsetY=e.offsetY}else if(e.layerX!==undefined){s.offsetX=e.layerX-e.currentTarget.offsetLeft;s.offsetY=e.layerY-e.currentTarget.offsetTop}}if(e.isPrimary!==undefined)s.isPrimary=e.isPrimary;else s.isPrimary=true;if(e.pressure)s.pressure=e.pressure;else{var o=0;if(e.which!==undefined)o=e.which;else if(e.button!==undefined){o=e.button}s.pressure=o==0?0:.5}if(e.rotation)s.rotation=e.rotation;else s.rotation=0;if(e.hwTimestamp)s.hwTimestamp=e.hwTimestamp;else s.hwTimestamp=0;if(e.tiltX)s.tiltX=e.tiltX;else s.tiltX=0;if(e.tiltY)s.tiltY=e.tiltY;else s.tiltY=0;if(e.height)s.height=e.height;else s.height=0;if(e.width)s.width=e.width;else s.width=0;s.preventDefault=function(){if(e.preventDefault!==undefined)e.preventDefault()};s.POINTER_TYPE_TOUCH=t;s.POINTER_TYPE_PEN=n;s.POINTER_TYPE_MOUSE=r;s.pointerId=e.pointerId;s.pointerType=e.pointerType;switch(s.pointerType){case 2:s.pointerType=s.POINTER_TYPE_TOUCH;break;case 3:s.pointerType=s.POINTER_TYPE_PEN;break;case 4:s.pointerType=s.POINTER_TYPE_MOUSE;break}if(e.currentTarget.handjs_forcePreventDefault===true)s.preventDefault();e.currentTarget.dispatchEvent(s)};var s=function(e,t){e.pointerId=1;e.pointerType=r;i(e,t)};var o=function(e,n){if(e.preventManipulation)e.preventManipulation();for(var r=0;r