Skip to content

Instantly share code, notes, and snippets.

@liquidzym
Forked from victusfate/watercanvas.js
Created June 11, 2011 14:22
Show Gist options
  • Save liquidzym/1020597 to your computer and use it in GitHub Desktop.
Save liquidzym/1020597 to your computer and use it in GitHub Desktop.
water canvas by Almer Thie http://code.almeros.com
/*
* 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);
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment