Behold the Ripples
The Code:
Caution: if the canvas is NOT at 0,0 in the window, you will need to adjust this code to correctly get the mouse position to send to dropAt(...) in the canvas.onmousemove function. See the mousePos example for more hints.
<!DOCTYPE html>
<html>
<head>
<title>Water Ripple HTML5</title>
<meta name="author" content="Brent Dingle">
<meta name="description" content="HTML5 canvas example of water ripple effect">
<style >
.waterCanvasStyle
{
border-width: 1px;
border-style: solid;
border-color:#a1a1d0;
border-radius: 8px;
box-shadow: #c6c6d0 4px 4px 10px;
}
</style>
</head>
<body>
<canvas id="waterCanvas0" width="400" height="400" >
Your browser does not support the HTML5 canvas tag.
</canvas>
<script>
var canvas = document.getElementById('waterCanvas0');
var ctx = canvas.getContext('2d');
var width = canvas.width;
var height = canvas.height;
var halfWidth = width >> 1;
var halfHeight = height >> 1;
var size = width * (height + 2) * 2; // space for 2 images (old and new), +2 to cover ripple radius <= 3
var delay = 30; // delay is desired FPS
var oldIdx = width;
var newIdx = width * (height + 3); // +2 from above size calc +1 more to get to 2nd image
var rippleRad = 3;
var rippleMap = [];
var lastMap = [];
var mapIdx;
// texture and ripple will hold the image data to be displayed
var ripple;
var texture;
// Any image can be used, but we will create a simple pattern instead
// So need some variables to create the background/underwater image
var stripeWidth = 25;
var step = stripeWidth * 2;
var count = height / stripeWidth;
canvas.width = width;
canvas.height = height;
// Here is a neat trick so you don't have to type ctx.blah over and over again
with (ctx)
{
fillStyle = '#008888';
fillRect(0, 0, width, height);
fillStyle = '#00ff77';
// Save the transform state
save();
// Perform rotation to make diagonal lines
rotate(-0.250);
for (var i = 0; i < count; i++)
{
// Go long on the width to make certain we draw
// across the entire viewable area --- with the rotation
// Yes. This can be done more exactly, but this should make
// it easier to play with the rotation value.
fillRect(-width, i * step, width * 3, stripeWidth);
}
// Restore the transform state
restore();
}
// Initialize the texture and ripple image data
// Texture will never be changed
// Ripple is what will be altered and displayed --> see run() function
texture = ctx.getImageData(0, 0, width, height);
ripple = ctx.getImageData(0, 0, width, height);
// Initialize the maps
for (var i = 0; i < size; i++)
{
lastMap[i] = 0;
rippleMap[i] = 0;
}
// -------------------------------------------------------
// --------------------- Main Run Loop --------------
// -------------------------------------------------------
function run()
{
newframe();
ctx.putImageData(ripple, 0, 0);
}
// -------------------------------------------------------
// Drop something in the water at location: dx, dy
// -------------------------------------------------------
function dropAt(dx, dy)
{
// Make certain dx and dy are integers
// Shifting left 0 is slightly faster than parseInt and math.* (or used to be)
dx <<= 0;
dy <<= 0;
// Our ripple effect area is actually a square, not a circle
for (var j = dy - rippleRad; j < dy + rippleRad; j++)
{
for (var k = dx - rippleRad; k < dx + rippleRad; k++)
{
rippleMap[oldIdx + (j * width) + k] += 512;
}
}
}
// -------------------------------------------------------
// Create the next frame of the ripple effect
// -------------------------------------------------------
function newframe()
{
var i;
var a, b;
var data, oldData;
var curPixel, newPixel;
// Store indexes - old and new may be misleading/confusing
// - current and next is slightly more accurate
// - previous and current may also help in thinking
i = oldIdx;
oldIdx = newIdx;
newIdx = i;
// Initialize the looping values - each will be incremented
i = 0;
mapIdx = oldIdx;
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
// Use rippleMap to set data value, mapIdx = oldIdx
// Use averaged values of pixels: above, below, left and right of current
data = (
rippleMap[mapIdx - width] +
rippleMap[mapIdx + width] +
rippleMap[mapIdx - 1] +
rippleMap[mapIdx + 1]) >> 1; // right shift 1 is same as divide by 2
// Subtract 'previous' value (we are about to overwrite rippleMap[newIdx+i])
data -= rippleMap[newIdx + i];
// Reduce value more -- for damping
// data = data - (data / 32)
data -= data >> 5;
// Set new value
rippleMap[newIdx + i] = data;
// If data = 0 then water is flat/still,
// If data > 0 then water has a wave
data = 1024 - data;
oldData = lastMap[i];
lastMap[i] = data;
if (oldData != data) // if no change no need to alter image
{
// Recall using "<< 0" forces integer value
// Calculate pixel offsets
a = (((x - halfWidth) * data / 1024) << 0) + halfWidth;
b = (((y - halfHeight) * data / 1024) << 0) + halfHeight;
// Don't go outside the image (i.e. boundary check)
if (a >= width) a = width - 1;
if (a < 0) a = 0;
if (b >= height) b = height - 1;
if (b < 0) b = 0;
// Set indexes
newPixel = (a + (b * width)) * 4;
curPixel = i * 4;
// Apply values
ripple.data[curPixel] = texture.data[newPixel];
ripple.data[curPixel + 1] = texture.data[newPixel + 1];
ripple.data[curPixel + 2] = texture.data[newPixel + 2];
}
mapIdx++;
i++;
}
}
}
// -------------------------------------------------------
// Select random location to create drops
// So if user is doing nothing, water still
// gets ripples.
// -------------------------------------------------------
function randomDrop()
{
// Make it a little, irregular in timing
if ( Math.random() > 0.3 )
{
dropAt(Math.random() * width, Math.random() * height);
}
}
// -------------------------------------------------------
// Adjust mouse position to account for canvas placement
// -------------------------------------------------------
function getMousePos(canvas, evt)
{
var rect = canvas.getBoundingClientRect();
return {
// add Math.round( ) around the below to get rid of excessive decimals
x: Math.round( (evt.clientX-rect.left)/(rect.right-rect.left)*canvas.width ),
y: Math.round( (evt.clientY-rect.top)/(rect.bottom-rect.top)*canvas.height )
};
}
// -------------------------------------------------------
// Event handler for mouse motion
// -------------------------------------------------------
canvas.onmousemove = function(/* Event */ evt)
{
//dropAt(evt.offsetX || evt.layerX, evt.offsetY || evt.layerY);
var mousePos = getMousePos(canvas, evt);
var mx = mousePos.x;
var my = mousePos.y;
dropAt(mx, my);
}
// -------------------------------------------------------
// Begin our infinite loop
// For user interaction and display updates
// -------------------------------------------------------
setInterval(run, delay);
// -------------------------------------------------------
// Create random ripples
// Note: this is NOT at same rate as display refresh
// -------------------------------------------------------
setInterval(randomDrop, 1250);
</script>
</body>
<html>
<head>
<title>Water Ripple HTML5</title>
<meta name="author" content="Brent Dingle">
<meta name="description" content="HTML5 canvas example of water ripple effect">
<style >
.waterCanvasStyle
{
border-width: 1px;
border-style: solid;
border-color:#a1a1d0;
border-radius: 8px;
box-shadow: #c6c6d0 4px 4px 10px;
}
</style>
</head>
<body>
<canvas id="waterCanvas0" width="400" height="400" >
Your browser does not support the HTML5 canvas tag.
</canvas>
<script>
var canvas = document.getElementById('waterCanvas0');
var ctx = canvas.getContext('2d');
var width = canvas.width;
var height = canvas.height;
var halfWidth = width >> 1;
var halfHeight = height >> 1;
var size = width * (height + 2) * 2; // space for 2 images (old and new), +2 to cover ripple radius <= 3
var delay = 30; // delay is desired FPS
var oldIdx = width;
var newIdx = width * (height + 3); // +2 from above size calc +1 more to get to 2nd image
var rippleRad = 3;
var rippleMap = [];
var lastMap = [];
var mapIdx;
// texture and ripple will hold the image data to be displayed
var ripple;
var texture;
// Any image can be used, but we will create a simple pattern instead
// So need some variables to create the background/underwater image
var stripeWidth = 25;
var step = stripeWidth * 2;
var count = height / stripeWidth;
canvas.width = width;
canvas.height = height;
// Here is a neat trick so you don't have to type ctx.blah over and over again
with (ctx)
{
fillStyle = '#008888';
fillRect(0, 0, width, height);
fillStyle = '#00ff77';
// Save the transform state
save();
// Perform rotation to make diagonal lines
rotate(-0.250);
for (var i = 0; i < count; i++)
{
// Go long on the width to make certain we draw
// across the entire viewable area --- with the rotation
// Yes. This can be done more exactly, but this should make
// it easier to play with the rotation value.
fillRect(-width, i * step, width * 3, stripeWidth);
}
// Restore the transform state
restore();
}
// Initialize the texture and ripple image data
// Texture will never be changed
// Ripple is what will be altered and displayed --> see run() function
texture = ctx.getImageData(0, 0, width, height);
ripple = ctx.getImageData(0, 0, width, height);
// Initialize the maps
for (var i = 0; i < size; i++)
{
lastMap[i] = 0;
rippleMap[i] = 0;
}
// -------------------------------------------------------
// --------------------- Main Run Loop --------------
// -------------------------------------------------------
function run()
{
newframe();
ctx.putImageData(ripple, 0, 0);
}
// -------------------------------------------------------
// Drop something in the water at location: dx, dy
// -------------------------------------------------------
function dropAt(dx, dy)
{
// Make certain dx and dy are integers
// Shifting left 0 is slightly faster than parseInt and math.* (or used to be)
dx <<= 0;
dy <<= 0;
// Our ripple effect area is actually a square, not a circle
for (var j = dy - rippleRad; j < dy + rippleRad; j++)
{
for (var k = dx - rippleRad; k < dx + rippleRad; k++)
{
rippleMap[oldIdx + (j * width) + k] += 512;
}
}
}
// -------------------------------------------------------
// Create the next frame of the ripple effect
// -------------------------------------------------------
function newframe()
{
var i;
var a, b;
var data, oldData;
var curPixel, newPixel;
// Store indexes - old and new may be misleading/confusing
// - current and next is slightly more accurate
// - previous and current may also help in thinking
i = oldIdx;
oldIdx = newIdx;
newIdx = i;
// Initialize the looping values - each will be incremented
i = 0;
mapIdx = oldIdx;
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
// Use rippleMap to set data value, mapIdx = oldIdx
// Use averaged values of pixels: above, below, left and right of current
data = (
rippleMap[mapIdx - width] +
rippleMap[mapIdx + width] +
rippleMap[mapIdx - 1] +
rippleMap[mapIdx + 1]) >> 1; // right shift 1 is same as divide by 2
// Subtract 'previous' value (we are about to overwrite rippleMap[newIdx+i])
data -= rippleMap[newIdx + i];
// Reduce value more -- for damping
// data = data - (data / 32)
data -= data >> 5;
// Set new value
rippleMap[newIdx + i] = data;
// If data = 0 then water is flat/still,
// If data > 0 then water has a wave
data = 1024 - data;
oldData = lastMap[i];
lastMap[i] = data;
if (oldData != data) // if no change no need to alter image
{
// Recall using "<< 0" forces integer value
// Calculate pixel offsets
a = (((x - halfWidth) * data / 1024) << 0) + halfWidth;
b = (((y - halfHeight) * data / 1024) << 0) + halfHeight;
// Don't go outside the image (i.e. boundary check)
if (a >= width) a = width - 1;
if (a < 0) a = 0;
if (b >= height) b = height - 1;
if (b < 0) b = 0;
// Set indexes
newPixel = (a + (b * width)) * 4;
curPixel = i * 4;
// Apply values
ripple.data[curPixel] = texture.data[newPixel];
ripple.data[curPixel + 1] = texture.data[newPixel + 1];
ripple.data[curPixel + 2] = texture.data[newPixel + 2];
}
mapIdx++;
i++;
}
}
}
// -------------------------------------------------------
// Select random location to create drops
// So if user is doing nothing, water still
// gets ripples.
// -------------------------------------------------------
function randomDrop()
{
// Make it a little, irregular in timing
if ( Math.random() > 0.3 )
{
dropAt(Math.random() * width, Math.random() * height);
}
}
// -------------------------------------------------------
// Adjust mouse position to account for canvas placement
// -------------------------------------------------------
function getMousePos(canvas, evt)
{
var rect = canvas.getBoundingClientRect();
return {
// add Math.round( ) around the below to get rid of excessive decimals
x: Math.round( (evt.clientX-rect.left)/(rect.right-rect.left)*canvas.width ),
y: Math.round( (evt.clientY-rect.top)/(rect.bottom-rect.top)*canvas.height )
};
}
// -------------------------------------------------------
// Event handler for mouse motion
// -------------------------------------------------------
canvas.onmousemove = function(/* Event */ evt)
{
//dropAt(evt.offsetX || evt.layerX, evt.offsetY || evt.layerY);
var mousePos = getMousePos(canvas, evt);
var mx = mousePos.x;
var my = mousePos.y;
dropAt(mx, my);
}
// -------------------------------------------------------
// Begin our infinite loop
// For user interaction and display updates
// -------------------------------------------------------
setInterval(run, delay);
// -------------------------------------------------------
// Create random ripples
// Note: this is NOT at same rate as display refresh
// -------------------------------------------------------
setInterval(randomDrop, 1250);
</script>
</body>