// shim layer with setTimeout fallback (Paul Irish) window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })(); // Based on http://www.dgp.toronto.edu/people/stam/reality/Research/pdf/GDC03.pdf var NavierStokes = function(settings){ this.init(settings); }; NavierStokes.prototype = { init : function(settings){ var defaultSettings = { resolution : 64, iterations : 10, fract : 1/4, diffusion : 1, gridmodify : 0, dt : 0.1, callbackUser : function(D, U, V, size){}, callbackDisplay : function(D, U, V, size){} }; if (this.settings === undefined){ this.settings = defaultSettings; } this._mergeRecursive(this.settings, settings); this.rows = this.settings.resolution + 2; this.arraySize = (this.settings.resolution+2)*(this.settings.resolution+2); this.U = new Float32Array(this.arraySize); this.V = new Float32Array(this.arraySize); this.D = new Float32Array(this.arraySize); this.U_prev = new Float32Array(this.arraySize); this.V_prev = new Float32Array(this.arraySize); this.D_prev = new Float32Array(this.arraySize); this.NullArray = new Float32Array(this.arraySize); // Precalculate lookup table for 2D > 1D array. this.IX = new Array(this.rows); for (var i = 0; i < this.rows; i++){ this.IX[i] = new Array(this.rows); for (var b = 0; b < this.rows; b++){ this.IX[i][b] = i+b*this.rows; } } // Init all Arrays. for (i = 0; i < this.arraySize; i++){ this.D_prev[i] = this.U_prev[i] = this.V_prev[i] = this.D[i] = this.U[i] = this.V[i] = this.NullArray[i] = 0.0; } //Init some vars based on the Resolution settings: this.calculateSettings(); }, clear : function(){ this.D.set(this.NullArray); this.U.set(this.NullArray); this.V.set(this.NullArray); }, // Getter Setter getResolution : function(){ return this.settings.resolution; }, getSettings : function(){ return this.settings; }, update : function(){ // Add user Action this.userAction(); // Cals velosity & density this.vel_step (this.U, this.V, this.U_prev, this.V_prev, this.settings.dt ); this.dens_step (this.D, this.D_prev, this.U, this.V, this.settings.dt ); this.settings.callbackDisplay(this.D, this.U, this.V, this.settings.resolution); }, calculateSettings : function(){ this.centerPos = (-0.5/this.settings.resolution) * (1 + this.settings.gridmodify); this.scale = this.settings.resolution * 0.5; this.dt0 = this.settings.dt * this.settings.resolution; this.p5 = this.settings.resolution + 0.5; }, userAction : function (){ this.D_prev.set(this.NullArray); this.U_prev.set(this.NullArray); this.V_prev.set(this.NullArray); this.settings.callbackUser(this.D_prev, this.U_prev, this.V_prev, this.settings.resolution); }, vel_step : function (u, v, u0, v0, dt ){ var tmp; this.add_source(u, u0, dt ); this.add_source(v, v0, dt ); tmp = u0; u0 = u; u = tmp; this.diffuse ( 1, u, u0, dt ); tmp = v0; v0 = v; v = tmp; this.diffuse ( 2, v, v0, dt ); this.project(u, v, u0, v0); tmp = u0; u0 = u; u = tmp; tmp = v0; v0 = v; v = tmp; this.advect(1, u, u0, u0, v0, dt); this.advect(2, v, v0, u0, v0, dt); this.project(u, v, u0, v0 ); }, dens_step : function (x, x0, u, v, dt){ var tmp; this.add_source (x, x0, dt); //SWAP ( x0, x ); this.diffuse (0, x0, x, dt ); //SWAP ( x0, x ); this.advect ( 0, x, x0, u, v, dt ); }, add_source : function (x, s, dt){ for (var i=0; i this.p5) x = this.p5; var i0 = x | 0; var i1 = i0 + 1; if (y < 0.5) y = 0.5 if (y > this.p5) y = this.p5; var j0 = y | 0; var j1 = j0 + 1; var s1 = x - i0; var s0 = 1 - s1; var t1 = y - j0; var t0 = 1 - t1; var toR1 = this.IX[0][j0]; var toR2 = this.IX[0][j1]; d[pos] = s0 * (t0 * d0[i0 + toR1] + t1 * d0[i0 + toR2]) + s1 * (t0 * d0[i1 + toR1] + t1 * d0[i1 + toR2]); } } this.set_bnd(b, d); }, // Calculate Boundary's set_bnd : function (b, x ){ var i, j; switch (b){ case 1 : for (i = 1; i <= this.settings.resolution; i++) { x[i] = x[i + this.rows]; x[this.IX[i][(this.settings.resolution+1)]] = x[this.IX[i][(this.settings.resolution)]]; x[this.IX[0][i]] = -x[this.IX[1][i]]; x[this.IX[(this.settings.resolution+1)][i]] = -x[this.IX[(this.settings.resolution)][i]]; } break; case 2 : for ( i = 1; i <= this.settings.resolution; i++) { x[i] = -x[i + this.rows]; x[this.IX[i][(this.settings.resolution+1)]] = -x[this.IX[i][(this.settings.resolution)]]; x[this.IX[0][i]] = x[this.IX[1][i]]; x[this.IX[(this.settings.resolution+1)][i]] = x[this.IX[(this.settings.resolution)][i]]; } break; default : for ( i = 1; i <= this.settings.resolution; i++) { x[i] = x[i + this.rows]; x[this.IX[i][(this.settings.resolution+1)]] = x[this.IX[i][(this.settings.resolution)]]; x[this.IX[0][i]] = x[this.IX[1][i]]; x[this.IX[(this.settings.resolution+1)][i]] = x[this.IX[(this.settings.resolution)][i]]; } } // Boundes of the Canvas var topPos = this.IX[0][this.settings.resolution+1]; x[0] = (x[1] + x[this.rows]) / 2; x[topPos] = (x[1 + topPos] + x[this.IX[this.settings.resolution][0]]) / 2; x[(this.settings.resolution+1)] = (x[this.settings.resolution] + x[(this.settings.resolution + 1) + this.rows]) / 2; x[(this.settings.resolution+1)+topPos] = (x[this.settings.resolution + topPos] + x[this.IX[this.settings.resolution+1][this.settings.resolution]]); }, // Merge Settings. _mergeRecursive : function(obj1, obj2) { for (var p in obj2) { if ( obj2[p].constructor==Object ) { obj1[p] = this._mergeRecursive(obj1[p], obj2[p]); } else { obj1[p] = obj2[p]; } } return obj1; } }; var Display = function(canvas){ this.colorFunctions = { "BW" : this.calcColorBW , "Color": this.calcColor, "User" : this.calcUserColor }; this.currColorFunc = "BW"; this.canvas = canvas; this.context = canvas.getContext("2d", {alpha : false}); this.supportImageData = !!this.context.getImageData; this.colorUser = { R : 0, G : 255, B : 0 } }; Display.prototype = { init : function (res){ this.calcResolution = res; this.imageData = null; this.showColors = false; this.line = null; }, density : function(D, U, V){ var r,g,b, x, y, d; if (this.supportImageData){ // Get Image Data if (this.imageData === null){ this.imageData = this.context.getImageData(0, 0, this.calcResolution, this.calcResolution); } for (x = 0; x < this.calcResolution; x++) { for (y = 0; y < this.calcResolution; y++){ var posC = (x + y * this.calcResolution) * 4; var pos = fluid.IX[x][y]; var cArray = this.colorFunctions[this.currColorFunc].call(this, D, U, V, pos); this.imageData.data[posC] = cArray[0]; // R this.imageData.data[posC + 1] = cArray[1]; // G this.imageData.data[posC + 2] = cArray[2]; // B this.imageData.data[posC + 3] = 255; //A } } this.context.putImageData(this.imageData, 0, 0); }else{ // Slow fallback for oldie for (x = 0; x < this.calcResolution; x++) { for (y = 0; y < this.calcResolution; y++) { var pos = fluid.IX[x][y]; var c =(D[pos] / 2); this.context.setFillColor(c , c, c , 1); this.context.fillRect(x, y, 1, 1); } } } // Draw the line for creating an Emitter if (this.line != null){ this.context.beginPath(); this.context.lineWidth = 1; this.context.strokeStyle = "rgb(255,255,255)"; this.context.moveTo(this.line[0][0],this.line[0][1]); this.context.lineTo(this.line[1][0],this.line[1][1]); this.context.stroke(); } }, setColorFunction: function(colorFuncName){ this.currColorFunc = "BW"; }, calcColorBW : function(D, U, V, pos){ var bw = (D[pos] * 255 / 6) | 0; return [bw, bw, bw]; }, calcColor : function(D, U, V, pos){ var r = Math.abs((U[pos] * 1300 ) | 0); var b = Math.abs((V[pos] * 1300 ) | 0); var g = (D[pos] * 255 / 6) | 0; return [r, g, b]; }, calcUserColor : function(D, U, V, pos){ var r = Math.abs((U[pos] *500* this.colorUser.R) | 0); var g = (D[pos] * this.colorUser.G) | 0; var b = Math.abs((V[pos] *500* this.colorUser.B ) | 0); return [r, g, b]; }, drawLine : function (a0, a1, scale){ var l0 = [a0[0]*scale, a0[1]*scale]; var l1 = [a1[0]*scale, a1[1]*scale]; this.line = [l0, l1]; }, removeLine : function(){this.line = null}, }; var user = { displaySize : 500, canvas : null, canvasOffset : null, scale : 0, mouseStart: [], mouseEnd : [], mouseLeftDown : false, mousePath : [], mouseRightDown : false, mouseRightStart : [], forceEmitters : [], insertedDensity : 50, init : function(canvas){ this.canvas = $(canvas); this.canvasOffset = this.canvas.offset(); var that = this; window.ontouchend = window.onmouseup = function(e){that.handleInputEnd(e);}; canvas.ontouchstart = canvas.onmousedown = function(e){that.handleInput(e);}; canvas.ontouchmove = canvas.onmousemove = function(e){that.handleInputMove(e);}; canvas.oncontextmenu = function(e){e.preventDefault();}; }, interact : function(D, U, V, size){ var x, y, pos, i ; if (this.mouseLeftDown){ var dx = this.mouseStart[0] - this.mouseEnd[0]; var dy = this.mouseStart[1] - this.mouseEnd[1]; var mousePathLength = Math.sqrt(dx * dx + dy * dy) | 0; mousePathLength = (mousePathLength < 1) ? 1 : mousePathLength; for ( i = 0; i < mousePathLength; i++) { x = (((this.mouseStart[0] - (i / mousePathLength) * dx)) * this.scale) | 0; y = (((this.mouseStart[1] - (i / mousePathLength) * dy)) * this.scale) | 0; pos = fluid.IX[x][y]; U[pos] = -dx / 6; V[pos] = -dy / 6; D[pos] = this.insertedDensity; } this.mouseStart[0] = this.mouseEnd[0]; this.mouseStart[1] = this.mouseEnd[1]; } for (i = 0;i