├── 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