/**
* jQuery Ripples plugin v0.1.0 / http://github.com/sirxemic/jquery.ripples
* MIT License
* @author sirxemic / http://sirxemic.com/
* @contribution Metadata Consulting (MDC) / http://metadataconsulting.blogspot.com - IE Fix, edge case
+ is deliberate - forces the parser to treat the part following the + as an expression. This is usually used for functions that are invoked immediately
**/
+function ($) {
var gl;
var $window = $(window); // There is only one window, so why not cache the jQuery-wrapped window?
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
}; // Stupid Chrome
function hasWebGLSupport() {
var canvas = document.createElement(\'canvas\');
var context = canvas.getContext(\'webgl\') || canvas.getContext(\'experimental-webgl\');
var result = context && context.getExtension(\'OES_texture_float\') && context.getExtension(\'OES_texture_float_linear\');
return result;
}
var supportsWebGL = hasWebGLSupport();
function createProgram(vertexSource, fragmentSource, uniformValues)
{
function compileSource(type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(\'compile error: \' + gl.getShaderInfoLog(shader));
}
return shader;
}
var program = {};
program.id = gl.createProgram();
gl.attachShader(program.id, compileSource(gl.VERTEX_SHADER, vertexSource));
gl.attachShader(program.id, compileSource(gl.FRAGMENT_SHADER, fragmentSource));
gl.linkProgram(program.id);
if (!gl.getProgramParameter(program.id, gl.LINK_STATUS)) {
throw new Error(\'link error: \' + gl.getProgramInfoLog(program.id));
}
// Fetch the uniform and attribute locations
program.uniforms = {};
program.locations = {};
gl.useProgram(program.id);
gl.enableVertexAttribArray(0);
var name, type, regex = /uniform (\\w+) (\\w+)/g, shaderCode = vertexSource + fragmentSource;
while ((match = regex.exec(shaderCode)) != null) {
name = match[2];
program.locations[name] = gl.getUniformLocation(program.id, name);
}
return program;
}
function bindTexture(texture, unit) {
gl.activeTexture(gl.TEXTURE0 + (unit || 0));
gl.bindTexture(gl.TEXTURE_2D, texture);
}
// Extend the css
$(\'head\').prepend(\'<style>.jquery-ripples { position: relative; z-index: 0; }</style>\');
// RIPPLES CLASS DEFINITION
// =========================
var Ripples = function (el, options) {
var that = this;
this.$el = $(el);
this.$el.addClass(\'jquery-ripples\');
// If this element doesn\'t have a background image, don\'t apply this effect to it
var backgroundUrl = (/url\\(["\']?([^"\']*)["\']?\\)/.exec(this.$el.css(\'background-image\')));
if (backgroundUrl == null) return;
backgroundUrl = backgroundUrl[1];
this.resolution = options.resolution || 256;
this.textureDelta = new Float32Array([1 / this.resolution, 1 / this.resolution]);
this.perturbance = options.perturbance;
this.dropRadius = options.dropRadius;
var canvas = document.createElement(\'canvas\');
canvas.width = this.$el.innerWidth();
canvas.height = this.$el.innerHeight();
this.canvas = canvas;
this.$canvas = $(canvas);
this.$canvas.css({
position: \'absolute\',
left: 0,
top: 0,
right: 0,
bottom: 0,
zIndex: -1
});
this.$el.append(canvas);
this.context = gl = canvas.getContext(\'webgl\') || canvas.getContext(\'experimental-webgl\');
// Load extensions
gl.getExtension(\'OES_texture_float\');
gl.getExtension(\'OES_texture_float_linear\');
// Init events
$(window).on(\'resize\', function() {
if (that.$el.innerWidth() != that.canvas.width || that.$el.innerHeight() != that.canvas.height) {
canvas.width = that.$el.innerWidth();
canvas.height = that.$el.innerHeight();
}
});
this.$el.on(\'mousemove.ripples\', function(e) {
if (that.visible) that.dropAtMouse(e, that.dropRadius, 0.01);
}).on(\'mousedown.ripples\', function(e) {
if (that.visible) that.dropAtMouse(e, that.dropRadius * 1.5, 0.14);
});
this.textures = [];
this.framebuffers = [];
for (var i = 0; i < 2; i++) {
var texture = gl.createTexture();
var framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
framebuffer.width = this.resolution;
framebuffer.height = this.resolution;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.resolution, this.resolution, 0, gl.RGBA, gl.FLOAT, null);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
throw new Error(\'Rendering to this texture is not supported (incomplete framebuffer)\');
}
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
this.textures.push(texture);
this.framebuffers.push(framebuffer);
}
// Init GL stuff
this.quad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.quad);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
+1, -1,
+1, +1,
-1, +1
]), gl.STATIC_DRAW);
this.initShaders();
// Init textures
var image = new Image;
image.crossOrigin = \'\';
image.onload = function() {
gl = that.context;
function isPowerOfTwo(x) {
return (x & (x - 1)) == 0;
}
var wrapping = (isPowerOfTwo(image.width) && isPowerOfTwo(image.height)) ? gl.REPEAT : gl.CLAMP_TO_EDGE;
that.backgroundWidth = image.width;
that.backgroundHeight = image.height;
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapping);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapping);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
that.backgroundTexture = texture;
// Everything loaded successfully - hide the CSS background image
that.$el.css(\'backgroundImage\', \'none\');
};
image.src = backgroundUrl;
this.visible = true;
// Init animation
function step() {
that.update();
requestAnimationFrame(step);
}
requestAnimationFrame(step);
};
Ripples.DEFAULTS = {
resolution: 256,
dropRadius: 20,
perturbance: 0.03
};
Ripples.prototype = {
update: function() {
gl = this.context;
if (!this.visible || !this.backgroundTexture) return;
this.updateTextures();
this.render();
},
drawQuad: function() {
gl.bindBuffer(gl.ARRAY_BUFFER, this.quad);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
},
render: function() {
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(this.renderProgram.id);
bindTexture(this.backgroundTexture, 0);
bindTexture(this.textures[0], 1);
gl.uniform2fv(this.renderProgram.locations.topLeft, this.renderProgram.uniforms.topLeft);
gl.uniform2fv(this.renderProgram.locations.bottomRight, this.renderProgram.uniforms.bottomRight);
gl.uniform2fv(this.renderProgram.locations.containerRatio, this.renderProgram.uniforms.containerRatio);
gl.uniform1i(this.renderProgram.locations.samplerBackground, 0);
gl.uniform1i(this.renderProgram.locations.samplerRipples, 1);
this.drawQuad();
},
updateTextures: function() {
this.computeTextureBoundaries();
gl.viewport(0, 0, this.resolution, this.resolution);
for (var i = 0; i < 2; i++) {
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[i]);
bindTexture(this.textures[1-i]);
gl.useProgram(this.updateProgram[i].id);
this.drawQuad();
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
},
computeTextureBoundaries: function() {
var backgroundSize = this.$el.css(\'background-size\');
var backgroundAttachment = this.$el.css(\'background-attachment\');
var backgroundPosition = this.$el.css(\'background-position\').split(\' \');
// Here the \'window\' is the element which the background adapts to
// (either the chrome window or some element, depending on attachment)
var parElement = backgroundAttachment == \'fixed\' ? $window : this.$el;
var winOffset = parElement.offset() || {left: pageXOffset, top: pageYOffset};
var winWidth = parElement.innerWidth();
var winHeight = parElement.innerHeight();
// TODO: background-clip
if (backgroundSize == \'cover\') {
var scale = Math.max(winWidth / this.backgroundWidth, winHeight / this.backgroundHeight);
var backgroundWidth = this.backgroundWidth * scale;
var backgroundHeight = this.backgroundHeight * scale;
}
else if (backgroundSize == \'contain\') {
var scale = Math.min(winWidth / this.backgroundWidth, winHeight / this.backgroundHeight);
var backgroundWidth = this.backgroundWidth * scale;
var backgroundHeight = this.backgroundHeight * scale;
}
else {
backgroundSize = backgroundSize.split(\' \');
var backgroundWidth = backgroundSize[0];
var backgroundHeight = backgroundSize[1] || backgroundSize[0];
if (backgroundWidth.endsWith(\'%\')) backgroundWidth = winWidth * parseFloat(backgroundWidth) / 100;
else if (backgroundWidth != \'auto\') backgroundWidth = parseFloat(backgroundWidth);
if (backgroundHeight.endsWith(\'%\')) backgroundHeight = winHeight * parseFloat(backgroundHeight) / 100;
else if (backgroundHeight != \'auto\') backgroundHeight = parseFloat(backgroundHeight);
if (backgroundWidth == \'auto\' && backgroundHeight == \'auto\') {
backgroundWidth = this.backgroundWidth;
backgroundHeight = this.backgroundHeight;
}
else {
if (backgroundWidth == \'auto\') backgroundWidth = this.backgroundWidth * (backgroundHeight / this.backgroundHeight);
if (backgroundHeight == \'auto\') backgroundHeight = this.backgroundHeight * (backgroundWidth / this.backgroundWidth);
}
}
// Sun 18-Jan-15 MDC - add edge case for IE 11
var backgroundX = (typeof backgroundPosition[0] == \'undefined\') ? \'\':backgroundPosition[0];
var backgroundY = (typeof backgroundPosition[1] == \'undefined\') ? \'\':backgroundPosition[1];
if (backgroundX != \'\') {
if (backgroundX == \'left\') backgroundX = winOffset.left;
else if (backgroundX == \'center\') backgroundX = winOffset.left + winWidth / 2 - backgroundWidth / 2;
else if (backgroundX == \'right\') backgroundX = winOffset.left + winWidth - backgroundWidth;
else if (backgroundX.endsWith(\'%\')) {
backgroundX = winOffset.left + (winWidth - backgroundWidth) * parseFloat(backgroundX) / 100;
}
else {
backgroundX = parseFloat(backgroundX);
}
}
if (backgroundY != \'\') {
if (backgroundY == \'top\') backgroundY = winOffset.top;
else if (backgroundY == \'center\') backgroundY = winOffset.top + winHeight / 2 - backgroundHeight / 2;
else if (backgroundY == \'bottom\') backgroundY = winOffset.top + winHeight - backgroundHeight;
else if (backgroundY.endsWith(\'%\')) {
backgroundY = winOffset.top + (winHeight - backgroundHeight) * parseFloat(backgroundY) / 100;
}
else {
backgroundY = parseFloat(backgroundY);
}
}
var elementOffset = this.$el.offset();
this.renderProgram.uniforms.topLeft = new Float32Array([
(elementOffset.left - backgroundX) / backgroundWidth,
(elementOffset.top - backgroundY) / backgroundHeight
]);
this.renderProgram.uniforms.bottomRight = new Float32Array([
this.renderProgram.uniforms.topLeft[0] + this.$el.innerWidth() / backgroundWidth,
this.renderProgram.uniforms.topLeft[1] + this.$el.innerHeight() / backgroundHeight
]);
var maxSide = Math.max(this.canvas.width, this.canvas.height);
this.renderProgram.uniforms.containerRatio = new Float32Array([
this.canvas.width / maxSide,
this.canvas.height / maxSide
]);
},
initShaders: function() {
var vertexShader = [
\'attribute vec2 vertex;\',
\'varying vec2 coord;\',
\'void main() {\',
\'coord = vertex * 0.5 + 0.5;\',
\'gl_Position = vec4(vertex, 0.0, 1.0);\',
\'}\'
].join(\'\\n\');
this.dropProgram = createProgram(vertexShader, [
\'precision highp float;\',
\'const float PI = 3.141592653589793;\',
\'uniform sampler2D texture;\',
\'uniform vec2 center;\',
\'uniform float radius;\',
\'uniform float strength;\',
\'varying vec2 coord;\',
\'void main() {\',
\'vec4 info = texture2D(texture, coord);\',
\'float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - coord) / radius);\',
\'drop = 0.5 - cos(drop * PI) * 0.5;\',
\'info.r += drop * strength;\',
\'gl_FragColor = info;\',
\'}\'
].join(\'\\n\'));
this.updateProgram = [0,0];
this.updateProgram[0] = createProgram(vertexShader, [
\'precision highp float;\',
\'uniform sampler2D texture;\',
\'uniform vec2 delta;\',
\'varying vec2 coord;\',
\'void main() {\',
\'vec4 info = texture2D(texture, coord);\',
\'vec2 dx = vec2(delta.x, 0.0);\',
\'vec2 dy = vec2(0.0, delta.y);\',
\'float average = (\',
\'texture2D(texture, coord - dx).r +\',
\'texture2D(texture, coord - dy).r +\',
\'texture2D(texture, coord + dx).r +\',
\'texture2D(texture, coord + dy).r\',
\') * 0.25;\',
\'info.g += (average - info.r) * 2.0;\',
\'info.g *= 0.995;\',
\'info.r += info.g;\',
\'gl_FragColor = info;\',
\'}\'
].join(\'\\n\'));
gl.uniform2fv(this.updateProgram[0].locations.delta, this.textureDelta);
this.updateProgram[1] = createProgram(vertexShader, [
\'precision highp float;\',
\'uniform sampler2D texture;\',
\'uniform vec2 delta;\',
\'varying vec2 coord;\',
\'void main() {\',
\'vec4 info = texture2D(texture, coord);\',
\'vec3 dx = vec3(delta.x, texture2D(texture, vec2(coord.x + delta.x, coord.y)).r - info.r, 0.0);\',
\'vec3 dy = vec3(0.0, texture2D(texture, vec2(coord.x, coord.y + delta.y)).r - info.r, delta.y);\',
\'info.ba = normalize(cross(dy, dx)).xz;\',
\'gl_FragColor = info;\',
\'}\'
].join(\'\\n\'));
gl.uniform2fv(this.updateProgram[1].locations.delta, this.textureDelta);
this.renderProgram = createProgram([
\'precision highp float;\',
\'attribute vec2 vertex;\',
\'uniform vec2 topLeft;\',
\'uniform vec2 bottomRight;\',
\'uniform vec2 containerRatio;\',
\'varying vec2 ripplesCoord;\',
\'varying vec2 backgroundCoord;\',
\'void main() {\',
\'backgroundCoord = mix(topLeft, bottomRight, vertex * 0.5 + 0.5);\',
\'backgroundCoord.y = 1.0 - backgroundCoord.y;\',
\'ripplesCoord = vec2(vertex.x, -vertex.y) * containerRatio * 0.5 + 0.5;\',
\'gl_Position = vec4(vertex.x, -vertex.y, 0.0, 1.0);\',
\'}\'
].join(\'\\n\'), [
\'precision highp float;\',
\'uniform sampler2D samplerBackground;\',
\'uniform sampler2D samplerRipples;\',
\'uniform float perturbance;\',
\'varying vec2 ripplesCoord;\',
\'varying vec2 backgroundCoord;\',
\'void main() {\',
\'vec2 offset = -texture2D(samplerRipples, ripplesCoord).ba;\',
\'float specular = pow(max(0.0, dot(offset, normalize(vec2(-0.6, 1.0)))), 4.0);\',
\'gl_FragColor = texture2D(samplerBackground, backgroundCoord + offset * perturbance) + specular;\',
\'}\'
].join(\'\\n\'));
gl.uniform1f(this.renderProgram.locations.perturbance, this.perturbance);
},
dropAtMouse: function(e, radius, strength) {
this.drop(
e.offsetX || (e.pageX - this.$el.offset().left),
e.offsetY || (e.pageY - this.$el.offset().top),
radius,
strength
);
},
drop: function(x, y, radius, strength) {
var that = this;
gl = this.context;
var elWidth = this.$el.outerWidth();
var elHeight = this.$el.outerHeight();
var longestSide = Math.max(elWidth, elHeight);
radius = radius / longestSide;
var dropPosition = new Float32Array([
(2 * x - elWidth) / longestSide,
(elHeight - 2 * y) / longestSide
]);
gl.viewport(0, 0, this.resolution, this.resolution);
// Render onto texture/framebuffer 0
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[0]);
// Using texture 1
bindTexture(this.textures[1]);
gl.useProgram(this.dropProgram.id);
gl.uniform2fv(this.dropProgram.locations.center, dropPosition);
gl.uniform1f(this.dropProgram.locations.radius, radius);
gl.uniform1f(this.dropProgram.locations.strength, strength);
this.drawQuad();
// Switch textures
var t = this.framebuffers[0]; this.framebuffers[0] = this.framebuffers[1]; this.framebuffers[1] = t;
t = this.textures[0]; this.textures[0] = this.textures[1]; this.textures[1] = t;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
},
// Actions
destroy: function() {
this.canvas.remove();
this.$el.off(\'.ripples\');
this.$el.css(\'backgroundImage\', \'\');
this.$el.removeClass(\'jquery-ripples\').data(\'ripples\', undefined);
},
show: function() {
this.$canvas.show();
this.$el.css(\'backgroundImage\', \'none\');
this.visible = true;
},
hide: function() {
this.$canvas.hide();
this.$el.css(\'backgroundImage\', \'\');
this.visible = false;
}
};
// RIPPLES PLUGIN DEFINITION
// ==========================
var old = $.fn.ripples;
$.fn.ripples = function(option) {
if (!supportsWebGL) throw new Error(\'Your browser does not support at least one of the following: WebGL, OES_texture_float extension, OES_texture_float_linear extension.\');
var args = (arguments.length > 1) ? Array.prototype.slice.call(arguments, 1) : undefined;
return this.each(function() {
var $this = $(this);
var data = $this.data(\'ripples\');
var options = $.extend({}, Ripples.DEFAULTS, $this.data(), typeof option == \'object\' && option);
if (!data && typeof option == \'string\' && option == \'destroy\') return;
if (!data) $this.data(\'ripples\', (data = new Ripples(this, options)));
else if (typeof option == \'string\') Ripples.prototype[option].apply(data, args);
});
}
$.fn.ripples.Constructor = Ripples;
// RIPPLES NO CONFLICT
// ====================
$.fn.ripples.noConflict = function() {
$.fn.ripples = old;
return this;
}
}(window.jQuery);