Team:Exeter/Template/watercanvas.js

From 2013.igem.org

/*

* Water Canvas by Almer Thie (http://code.almeros.com).
* Description: A realtime water ripple effect on an HTML5 canvas. 
* Copyright 2010 Almer Thie. All rights reserved.
*
* Example: http://code.almeros.com/code-examples/water-effect-canvas/
* Tutorial: http://code.almeros.com/water-ripple-canvas-and-javascript
*/


//////////////////////////////////////////////////////////////////////////////// // WaterCanvas object // ////////////////////////////////////////////////////////////////////////////////

/**

* This view object is responsible for applying the water ripple data state to an image and therefor 
* is also responsible for the refraction effect of the light through the water.
* 
* @param {Number} width The width in pixels this canvas should be.
* @param {Number} hight The hight in pixels this canvas should be.
* @param {String} documentElement The div element ID to insert the canvas into.
* @param {Object} waterModel A reference to a WaterModel object previously created.
* @param {Object} props A key/value object which may contain the following properties:
* 				backgroundImageUrl: 
*						A relative URL to an image on your webserver. This cannot be a URL to an 
*						image on another domain. The HTML5 Canvas element follows the Same Origin Policy.
*				lightRefraction: 
*						Sets how many pixels the refraction of light uses.
* 				lightReflection: 
*						Sets the amount of color highlighting.
* 				maxFps: 
*						Maximum Frames Per Second; The maximum reachable FPS highly depends on the system and 
*						browser the water canvas is running on. You can't control that, but you can control the maximum 
* 						FPS to keep systems from overloading, trying to reach the highest FPS.
* 				showStats: 
*						When set to true, it shows "FPS Canvas: " plus the current FPS of this view on the canvas.
*
* @constructor
*/

WaterCanvas = function(width, height, documentElement, waterModel, props) { // If a certain property is not set, use a default value props = props || {}; this.backgroundImageUrl = props.backgroundImageUrl || null; this.lightRefraction = props.lightRefraction || 9.0; this.lightReflection = props.lightReflection || 0.1; this.showStats = props.showStats || false;

this.width = width; this.height = height; this.documentElement = document.getElementById(documentElement); this.waterModel = waterModel;


   this.canvas = document.createElement('canvas');  
   this.canvasHelp = document.createElement('canvas'); 

if(!this.canvas.getContext || !this.canvasHelp.getContext){ alert("You need a browser that supports the HTML5 canvas tag."); return; // No point to continue }

this.ctx = this.canvas.getContext('2d'); this.ctxHelp = this.canvasHelp.getContext('2d');

this.setSize(width, height);

   this.documentElement.appendChild(this.canvas);  



// Find out the FPS at certain intervals this.fps = -1; if(this.showStats){ this.fpsCounter = 0; this.prevMs = 0;

var self = this; setInterval(function(){ self.findFps(); }, 1000); }


// Start the animation this.drawNextFrame(); }

/**

* Sets the size of the canvas.
*
* @param {Number} width The width in pixels this canvas should be.
* @param {Number} hight The hight in pixels this canvas should be.
*/

WaterCanvas.prototype.setSize = function(width, height){ this.width = width; this.height = height;

   this.canvas.setAttribute('width', this.width);  
   this.canvas.setAttribute('height', this.height); 
   this.canvasHelp.setAttribute('width', this.width);  
   this.canvasHelp.setAttribute('height', this.height);  	

this.setBackground(this.backgroundImageUrl); }

/**

* Sets the image to perform the water rippling effect on. 
*
* Use an image from the same server as where this script lives. This is needed since browsers use the 
* Same Origin Policy for savety reasons. If an empty URL is given, a standard canvas will be shown.
*
* @param {String} backgroundImageUrl (Relative) URL to an image on the same webserver as this script.						
*/

WaterCanvas.prototype.setBackground = function(backgroundImageUrl){ this.backgroundImageUrl = backgroundImageUrl=="" ? null : backgroundImageUrl; this.pixelsIn = null;

if(this.backgroundImageUrl!=null){

// Background image loading this.backgroundImg = new Image();

var self = this; this.backgroundImg.onload = function(){ self.ctxHelp.drawImage(self.backgroundImg, 0, 0, self.width, self.height);

// Get the canvas pixel data var imgDataIn = self.ctxHelp.getImageData(0, 0, self.width, self.height); self.pixelsIn = imgDataIn.data;

// Also paint it on the output canvas self.ctx.putImageData(imgDataIn, 0, 0); } this.backgroundImg.src = this.backgroundImageUrl;

} else {

var radgrad = pointerCtx.createRadialGradient(this.width/2,this.height/2,0, this.width/2,this.height/2,this.height/2); radgrad.addColorStop(0, '#4af'); radgrad.addColorStop(1, '#000');

this.ctxHelp.fillStyle = radgrad; this.ctxHelp.fillRect(0,0,this.width,this.height);

this.ctxHelp.shadowColor = "white"; this.ctxHelp.shadowOffsetX = 0; this.ctxHelp.shadowOffsetY = 0; this.ctxHelp.shadowBlur = 10;

this.ctxHelp.textBaseline = "top"; this.ctxHelp.font = 'normal 200 45px verdana'; this.ctxHelp.fillStyle = "white"; this.ctxHelp.fillText("Water Canvas", 10, (this.height/2)-40); this.ctxHelp.font = 'normal 200 12px verdana'; this.ctxHelp.fillText("Move your mouse over this canvas to move the water.", 10, (this.height/2)+10); this.ctxHelp.fillText("By Almeros 2010, See http://code.almeros.com", 10, (this.height/2)+30);

// Get the canvas pixel data var imgDataIn = this.ctxHelp.getImageData(0, 0, this.width, this.height); this.pixelsIn = imgDataIn.data;

// Also paint it on the output canvas this.ctx.putImageData(imgDataIn, 0, 0);

} }

/**

* Renders the next frame and draws it on the canvas. 
* Also handles calling itself again via requestAnim(ation)Frame.
*
* @private
*/

WaterCanvas.prototype.drawNextFrame = function(){ if(this.pixelsIn==null || !this.waterModel.isEvolving()){ // Wait some time and try again var self = this; setTimeout( function(){ self.drawNextFrame(); }, 50 );

// Nothing else to do for now return; }


// Make the canvas give us a CanvasDataArray. // Creating an array ourselves is slow!!! // https://developer.mozilla.org/en/HTML/Canvas/Pixel_manipulation_with_canvas var imgDataOut = this.ctx.getImageData(0, 0, this.width, this.height); var pixelsOut = imgDataOut.data; for (var i = 0; n = pixelsOut.length, i < n; i += 4) { var pixel = i/4; var x = pixel % this.width; var y = (pixel-x) / this.width;

var strength = this.waterModel.getWater(x,y);


// Refraction of light in water var refraction = Math.round(strength * this.lightRefraction);

var xPix = x + refraction; var yPix = y + refraction;

if(xPix < 0) xPix = 0; if(yPix < 0) yPix = 0; if(xPix > this.width-1) xPix = this.width-1; if(yPix > this.height-1) yPix = this.height-1;


// Get the pixel from input var iPix = ((yPix * this.width) + xPix) * 4; var red = this.pixelsIn[iPix ]; var green = this.pixelsIn[iPix+1]; var blue = this.pixelsIn[iPix+2];


// Set the pixel to output strength *= this.lightReflection; strength += 1.0;

pixelsOut[i ] = red *= strength; pixelsOut[i+1] = green *= strength; pixelsOut[i+2] = blue *= strength; pixelsOut[i+3] = 255; // alpha }

this.ctx.putImageData(imgDataOut, 0,0);


if(this.showStats){ this.fpsCounter++;


this.ctx.textBaseline = "top"; this.ctx.font = 'normal 200 10px arial';

this.ctx.fillStyle = "white"; this.ctx.fillText ("FPS Canvas: " + this.getFps(), 10, 10); this.ctx.fillText ("FPS Water: " + this.waterModel.getFps(), 10, 20);

this.ctx.shadowColor = "black"; this.ctx.shadowOffsetX = 0; this.ctx.shadowOffsetY = 0; this.ctx.shadowBlur = 2; }


// Make the browser call this function at a new render frame var self = this; // For referencing 'this' in internal eventListeners requestAnimFrame( function(){ self.drawNextFrame() }, this.canvas );

}

/**

* Determine the Frames Per Second. 
* Called at regular intervals by an internal setInterval.
*
* @private
*/

WaterCanvas.prototype.findFps = function(){ if(!this.showStats) return;

var nowMs = new Date().getTime(); var diffMs = nowMs - this.prevMs;

this.fps = Math.round( ((this.fpsCounter*1000) / diffMs) * 10.0 ) / 10.0;

this.prevMs = nowMs; this.fpsCounter = 0; }

/**

* @returns The Frames Per Second that was last rendered.
*/

WaterCanvas.prototype.getFps = function(){ return this.fps; }

/**

* Sets the a amount of refraction of light through the water.
*
* @param {Number} lightRefraction The a amount of refraction of light.
*/

WaterCanvas.prototype.setLightRefraction = function(lightRefraction){ this.lightRefraction = lightRefraction; }

/**

* Sets the a amount of reflection of light on the water.
*
* @param {Number} lightRefraction The a amount of reflection of light.
*/

WaterCanvas.prototype.setLightReflection = function(lightReflection){ this.lightReflection = lightReflection; }



//////////////////////////////////////////////////////////////////////////////// // WaterModel object // ////////////////////////////////////////////////////////////////////////////////

/**

* Model object that is responsible for holding, manipulating and updating the water ripple data.
* 
* @param {Number} width The width in pixels this model should work with.
* @param {Number} hight The hight in pixels this model should work with.
* @param {Object} props A key/value object which may contain the following properties:
* 				resolution:  
*						Sets the pixelsize to use in the model. The higher hte better the performance, but 
*						you may want to use interpolation to prefent visible block artifacts.
*				interpolate: 
*						When the resolution is set higher then 1.0, you can set this to true to use interpolation.
*				damping: 
*						Effectively sets how long water ripples travel.
*				clipping: 
*						Multiple waves add up. This may create a wave that's to high or low. The clipping value sets 
*						the absolute maximum a water pixel value may have in the model.
*				evolveThreshold: 
*						To prevent this  script from always taking up CPU cycles, even when no water rippling 
*						is going on in the model (your website is on a non visible tab) you can set a threshold. When 
*						no water pixel is above this absolute value, the WaterModel and the WaterCanvas will both stop 
*						rendering until new waves are created by the user.
*				maxFps: 
*						Maximum Frames Per Second; The maximum reachable FPS highly depends on the system and browser 
*						the water canvas is running on. You can't control that, but you can control the maximum FPS to 
*						keep systems from overloading, trying to reach the highest FPS.
*				showStats: 
*						When set to true, it shows "FPS Water: " plus the current FPS of this model on the canvas.
*
* @constructor
*/

WaterModel = function(width, height, props) { // If a certain property is not set, use a default value props = props || {}; this.resolution = props.resolution || 2.0; this.interpolate = props.interpolate || false; this.damping = props.damping || 0.985; this.clipping = props.clipping || 5; this.maxFps = props.maxFps || 30; this.showStats = props.showStats || false; this.evolveThreshold = props.evolveThreshold || 0.05;

this.width = Math.ceil(width/this.resolution); this.height = Math.ceil(height/this.resolution);


// Create water model 2D arrays this.resetSizeAndResolution(width, height, this.resolution) this.swapMap;

this.setMaxFps(this.maxFps);

this.evolving = false; // Holds whether it's needed to render frames


// Find out the FPS at certain intervals this.fps = -1; if(this.showStats){ this.fpsCounter = 0; this.prevMs = 0;

var self = this; setInterval(function(){ self.findFps(); }, 1000); } }

/**

* Gets the (interpolated) water value of an coordinate.
*
* @param {Number} x The X position.
* @param {Number} y The Y position.
*
* @returns A float value representing the hight of the water.
*/

WaterModel.prototype.getWater = function(x, y){ xTrans = x/this.resolution; yTrans = y/this.resolution;

if(!this.interpolate || this.resolution==1.0){ xF = Math.floor(xTrans); yF = Math.floor(yTrans);

if(xF>this.width-1 || yF>this.height-1) return 0.0;

return this.depthMap1[xF][yF]; }


// Else use Bilinear Interpolation xF = Math.floor(xTrans); yF = Math.floor(yTrans); xC = Math.ceil(xTrans); yC = Math.ceil(yTrans);

if(xC>this.width-1 || yC>this.height-1) return 0.0;

// Now get 4 points from the array var br = this.depthMap1[xF][yF]; var bl = this.depthMap1[xC][yF]; var tr = this.depthMap1[xF][yC]; var tl = this.depthMap1[xC][yC];

// http://tech-algorithm.com/articles/bilinear-image-scaling/ // D C // Y // B A // Y = A(1-w)(1-h) + B(w)(1-h) + C(h)(1-w) + Dwh

var xChange = xC - xTrans; var yChange = yC - yTrans; var intpVal = tl*(1-xChange) * (1-yChange) + tr*(xChange) * (1-yChange) + bl*(yChange) * (1-xChange) + br*xChange * yChange;

return intpVal; }

/**

* Sets bilinear interpolation on or off. Interpolation will give a more smooth effect
* when a higher resolution is used, but needs CPU resources for that.
*
* @param {Boolean} interpolate Whether to use interpolation or not
*/

WaterModel.prototype.setInterpolation = function(interpolate){ this.interpolate = interpolate; }

/**

* Gets the (interpolated) water value of an coordinate.
*
* @param {Number} x The X position. The center of where the array2d will be placed.
* @param {Number} y The Y position. The center of where the array2d will be placed.
* @param {Number} pressure The factor to multiply the array2d values with while adding the array2d to the model.
* @param {Array} array2d A 2D array containing float values between -1.0 and 1.0 in a pattern.
*/

WaterModel.prototype.touchWater = function(x, y, pressure, array2d){ this.evolving = true;

x = Math.floor(x/this.resolution); y = Math.floor(y/this.resolution);

// Place the array2d in the center of the mouse position if(array2d.length>4 || array2d[0].length>4){ x-=array2d.length/2; y-=array2d[0].length/2; }

if(x<0) x = 0; if(y<0) y = 0; if(x>this.width) x = this.width; if(y>this.height) y = this.height;

// Big pixel block for(var i = 0; i < array2d.length; i++){ for(var j = 0; j < array2d[0].length; j++){

if(x+i>=0 && y+j>=0 && x+i<=this.width-1 && y+j<=this.height-1) { this.depthMap1[x+i][y+j] -= array2d[i][j] * pressure; }

} } }

/**

* Renders the next frame in the model. The water ripples will be evolved one step.
* Called at regular intervals by an internal setInterval.
*
* @private
*/

WaterModel.prototype.renderNextFrame = function() { if(!this.evolving) return;

this.evolving = false;

for (var x = 0; x < this.width; x++) { for (var y = 0; y < this.height; y++) {

// Handle borders correctly var val = (x==0 ? 0 : this.depthMap1[x - 1][y]) + (x==this.width-1 ? 0 : this.depthMap1[x + 1][y]) + (y==0 ? 0 : this.depthMap1[x][y - 1]) + (y==this.height-1 ? 0 : this.depthMap1[x][y + 1]);

// Damping val = ((val / 2.0) - this.depthMap2[x][y]) * this.damping;

// Clipping prevention if(val>this.clipping) val = this.clipping; if(val<-this.clipping) val = -this.clipping;

// Evolve check if(Math.abs(val)>this.evolveThreshold) this.evolving = true;


this.depthMap2[x][y] = val; } }

// Swap buffer references this.swapMap = this.depthMap1; this.depthMap1 = this.depthMap2; this.depthMap2 = this.swapMap;

this.fpsCounter++; }

/**

* Tells if the WaterModel is currently evolving. When all postions in the model are below a threshold 
* (evolveThreshold), evolving will be set to false. This saves resources, especially when the canvas 
* is not visible on screen.
*
* @returns A boolean that tells if the WaterModel is currently in evolving state.
*/

WaterModel.prototype.isEvolving = function() { return this.evolving; }

/**

* Determine the Frames Per Second. 
* Called at regular intervals by an internal setInterval.
*
* @private
*/

WaterModel.prototype.findFps = function(){ if(!this.showStats) return;

var nowMs = new Date().getTime(); var diffMs = nowMs - this.prevMs;

this.fps = Math.round( ((this.fpsCounter*1000) / diffMs) * 10.0 ) / 10.0;

this.prevMs = nowMs; this.fpsCounter = 0; }

/**

* @returns The Frames Per Second that was last rendered.
*/

WaterModel.prototype.getFps = function(){ return this.fps; }

/**

* Sets the maximum frames per second to render. Use this to set a limit and release resources for other processes.
*
* @param {Number} maxFps The maximum frames per second to render.
*/

WaterModel.prototype.setMaxFps = function(maxFps){ this.maxFps = maxFps;

clearInterval(this.maxFpsInterval);

// Updating of the animation var self = this; // For referencing 'this' in internal eventListeners

if(this.maxFps>0){ this.maxFpsInterval = setInterval(function(){ self.renderNextFrame(); }, 1000/this.maxFps); } }

/**

* Effectively sets how long water ripples travel.
*
* @param {Number} damping The amount of strength to pass on from postion to surrounding positions.
*/

WaterModel.prototype.setDamping = function(damping){ this.damping = damping; }

/**

* Effectively sets how long water ripples travel.
* 
* @param {Number} width The width in pixels this model should work with.
* @param {Number} hight The hight in pixels this model should work with.
* @param {Number} resolution The pixel size of a models position. The higher the resolution, the less positions to render, the faster. 
*/

WaterModel.prototype.resetSizeAndResolution = function(width, height, resolution){ this.width = Math.ceil(width/resolution); this.height = Math.ceil(height/resolution); this.resolution = resolution;

this.depthMap1 = new Array(this.width); this.depthMap2 = new Array(this.width); for(var x = 0; x < this.width; x++){ this.depthMap1[x] = new Array(this.height); this.depthMap2[x] = new Array(this.height);

for (var y = 0; y < this.height; y++) { this.depthMap1[x][y] = 0.0; this.depthMap2[x][y] = 0.0; } } }




//////////////////////////////////////////////////////////////////////////////// // Util functions // ////////////////////////////////////////////////////////////////////////////////

/**

* A class to mimic rain on the given waterModel with raindrop2dArray's as raindrops.
*/

RainMaker = function(width, height, waterModel, raindrop2dArray) { this.width = width; this.height = height; this.waterModel = waterModel; this.raindrop2dArray = raindrop2dArray;

this.rainMinPressure = 1; this.rainMaxPressure = 3; }

RainMaker.prototype.raindrop = function(){ var x = Math.floor(Math.random() * this.width); var y = Math.floor(Math.random() * this.height); this.waterModel.touchWater(x, y, this.rainMinPressure + Math.random() * this.rainMaxPressure, this.raindrop2dArray); }

RainMaker.prototype.setRaindropsPerSecond = function(rps){ this.rps = rps;

clearInterval(this.rainInterval);

if(this.rps>0) { var self = this; this.rainInterval = setInterval(function(){ self.raindrop(); }, 1000/this.rps); } }

RainMaker.prototype.setRainMinPressure = function(rainMinPressure){ this.rainMinPressure = rainMinPressure; }

RainMaker.prototype.setRainMaxPressure = function(rainMaxPressure){ this.rainMaxPressure = rainMaxPressure; }

/**

* Enables mouse interactivity by adding event listeners to the given documentElement and
* using the mouse coordinates to 'touch' the water.
*/

function enableMouseInteraction(waterModel, documentElement){ var mouseDown = false;

var canvasHolder = document.getElementById(documentElement);

canvasHolder.addEventListener("mousedown", function(e){ mouseDown = true; var x = (e.clientX - canvasHolder.offsetLeft) + document.body.scrollLeft + document.documentElement.scrollLeft; var y = (e.clientY - canvasHolder.offsetTop) + document.body.scrollTop + document.documentElement.scrollTop; waterModel.touchWater(x, y, 1.5, mouseDown ? finger : pixel); }, false);

canvasHolder.addEventListener("mouseup", function(e){ mouseDown = false; }, false);

canvasHolder.addEventListener("mousemove", function(e){ var x = (e.clientX - canvasHolder.offsetLeft) + document.body.scrollLeft + document.documentElement.scrollLeft; var y = (e.clientY - canvasHolder.offsetTop) + document.body.scrollTop + document.documentElement.scrollTop; // mozPressure: https://developer.mozilla.org/en/DOM/Event/UIEvent/MouseEvent waterModel.touchWater(x, y, 1.5, mouseDown ? finger : pixel); }, false); }

/**

* Creates a canvas with a radial gradient from white in the center to black on the outside.
*/

function createRadialCanvas(width, height){ // Create a canvas var pointerCanvas = document.createElement('canvas'); pointerCanvas.setAttribute('width', width); pointerCanvas.setAttribute('height', height); pointerCtx = pointerCanvas.getContext('2d');

// Create a drawing on the canvas var radgrad = pointerCtx.createRadialGradient(width/2,height/2,0, width/2,height/2,height/2); radgrad.addColorStop(0, '#fff'); radgrad.addColorStop(1, '#000');

pointerCtx.fillStyle = radgrad; pointerCtx.fillRect(0,0,width,height);

return pointerCanvas; }

/**

* Creates a 2D pointer array from a given canvas with a grayscale image on it. 
* This canvas image is then converted to a 2D array with values between -1.0 and 0.0.
* 
* Example:
* 	var array2d = [
* 		[0.5, 1.0, 0.5], 
* 		[1.0, 1.0, 1.0], 
* 		[0.5, 1.0, 0.5]
* 	];
*/

function create2DArray(canvas){ var width = canvas.width; var height = canvas.height;

// Create an empty 2D array var pointerArray = new Array(width); for(var x = 0; x < width; x++){ pointerArray[x] = new Array(height); for (var y = 0; y < height; y++) { pointerArray[x][y] = 0.0; } }

// Convert gray scale canvas to 2D array var pointerCtx = canvas.getContext('2d'); var imgData = pointerCtx.getImageData(0, 0, width, height); var pixels = imgData.data;

for (var i = 0; n = pixels.length, i < n; i += 4) { // Get the pixel from input var pixVal = pixels[i];// only use red var arrVal = pixVal/255.0;

var pixel = i/4; var x = pixel % width; var y = (pixel-x) / width;

pointerArray[x][y] = arrVal; }

return pointerArray; }

// requestAnimFrame (NB: NOT requestAnimationFrame) will be used, // so make sure it's available. Credits @mrdoob // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ window.requestAnimFrame = (function(){

 return  window.requestAnimationFrame       || 

window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(/* function */ callback, /* DOMElement */ element){ window.setTimeout(callback, 1000 / 60); }; })();