/** * jquery ripples plugin v0.6.3 / https://github.com/sirxemic/jquery.ripples * mit license * @author sirxemic / https://sirxemic.com/ */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery')) : typeof define === 'function' && define.amd ? define(['jquery'], factory) : (factory(global.$)); }(this, (function ($) { 'use strict'; $ = $ && 'default' in $ ? $['default'] : $; var gl; var $window = $(window); // there is only one window, so why not cache the jquery-wrapped window? function ispercentage(str) { return str[str.length - 1] == '%'; } /** * load a configuration of gl settings which the browser supports. * for example: * - not all browsers support webgl * - not all browsers support floating point textures * - not all browsers support linear filtering for floating point textures * - not all browsers support rendering to floating point textures * - some browsers *do* support rendering to half-floating point textures instead. */ function loadconfig() { var canvas = document.createelement('canvas'); gl = canvas.getcontext('webgl') || canvas.getcontext('experimental-webgl'); if (!gl) { // browser does not support webgl. return null; } // load extensions var extensions = {}; [ 'oes_texture_float', 'oes_texture_half_float', 'oes_texture_float_linear', 'oes_texture_half_float_linear' ].foreach(function(name) { var extension = gl.getextension(name); if (extension) { extensions[name] = extension; } }); // if no floating point extensions are supported we can bail out early. if (!extensions.oes_texture_float) { return null; } var configs = []; function createconfig(type, gltype, arraytype) { var name = 'oes_texture_' + type, namelinear = name + '_linear', linearsupport = namelinear in extensions, configextensions = [name]; if (linearsupport) { configextensions.push(namelinear); } return { type: gltype, arraytype: arraytype, linearsupport: linearsupport, extensions: configextensions }; } configs.push( createconfig('float', gl.float, float32array) ); if (extensions.oes_texture_half_float) { configs.push( // array type should be uint16array, but at least on ios that breaks. in that case we // just initialize the textures with data=null, instead of data=new uint16array(...). // this makes initialization a tad slower, but it's still negligible. createconfig('half_float', extensions.oes_texture_half_float.half_float_oes, null) ); } // setup the texture and framebuffer var texture = gl.createtexture(); var framebuffer = gl.createframebuffer(); gl.bindframebuffer(gl.framebuffer, framebuffer); gl.bindtexture(gl.texture_2d, texture); gl.texparameteri(gl.texture_2d, gl.texture_min_filter, gl.nearest); gl.texparameteri(gl.texture_2d, gl.texture_mag_filter, gl.nearest); 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); // check for each supported texture type if rendering to it is supported var config = null; for (var i = 0; i < configs.length; i++) { gl.teximage2d(gl.texture_2d, 0, gl.rgba, 32, 32, 0, gl.rgba, configs[i].type, null); gl.framebuffertexture2d(gl.framebuffer, gl.color_attachment0, gl.texture_2d, texture, 0); if (gl.checkframebufferstatus(gl.framebuffer) === gl.framebuffer_complete) { config = configs[i]; break; } } return config; } function createimagedata(width, height) { try { return new imagedata(width, height); } catch (e) { // fallback for ie var canvas = document.createelement('canvas'); return canvas.getcontext('2d').createimagedata(width, height); } } function translatebackgroundposition(value) { var parts = value.split(' '); if (parts.length === 1) { switch (value) { case 'center': return ['50%', '50%']; case 'top': return ['50%', '0']; case 'bottom': return ['50%', '100%']; case 'left': return ['0', '50%']; case 'right': return ['100%', '50%']; default: return [value, '50%']; } } else { return parts.map(function(part) { switch (value) { case 'center': return '50%'; case 'top': case 'left': return '0'; case 'right': case 'bottom': return '100%'; default: return part; } }); } } 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 match, name, 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); } function extracturl(value) { var urlmatch = /url\(["']?([^"']*)["']?\)/.exec(value); if (urlmatch == null) { return null; } return urlmatch[1]; } function isdatauri(url) { return url.match(/^data:/); } var config = loadconfig(); var transparentpixels = createimagedata(32, 32); // extend the css $('head').prepend(''); // ripples class definition // ========================= var ripples = function (el, options) { var that = this; this.$el = $(el); // init properties from options this.interactive = options.interactive; this.resolution = options.resolution; this.texturedelta = new float32array([1 / this.resolution, 1 / this.resolution]); this.perturbance = options.perturbance; this.dropradius = options.dropradius; this.crossorigin = options.crossorigin; this.imageurl = options.imageurl; // init webgl canvas 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.addclass('jquery-ripples').append(canvas); this.context = gl = canvas.getcontext('webgl') || canvas.getcontext('experimental-webgl'); // load extensions config.extensions.foreach(function(name) { gl.getextension(name); }); // auto-resize when window size changes. this.updatesize = this.updatesize.bind(this); $(window).on('resize', this.updatesize); // init rendertargets for ripple data. this.textures = []; this.framebuffers = []; this.bufferwriteindex = 0; this.bufferreadindex = 1; var arraytype = config.arraytype; var texturedata = arraytype ? new arraytype(this.resolution * this.resolution * 4) : null; for (var i = 0; i < 2; i++) { var texture = gl.createtexture(); var framebuffer = gl.createframebuffer(); gl.bindframebuffer(gl.framebuffer, framebuffer); gl.bindtexture(gl.texture_2d, texture); gl.texparameteri(gl.texture_2d, gl.texture_min_filter, config.linearsupport ? gl.linear : gl.nearest); gl.texparameteri(gl.texture_2d, gl.texture_mag_filter, config.linearsupport ? gl.linear : gl.nearest); 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, config.type, texturedata); gl.framebuffertexture2d(gl.framebuffer, gl.color_attachment0, gl.texture_2d, texture, 0); 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(); this.inittexture(); this.settransparenttexture(); // load the image either from the options or css rules this.loadimage(); // set correct clear color and blend mode (regular alpha blending) gl.clearcolor(0, 0, 0, 0); gl.blendfunc(gl.src_alpha, gl.one_minus_src_alpha); // plugin is successfully initialized! this.visible = true; this.running = true; this.inited = true; this.destroyed = false; this.setuppointerevents(); // init animation function step() { if (!that.destroyed) { that.step(); requestanimationframe(step); } } requestanimationframe(step); }; ripples.defaults = { imageurl: null, resolution: 256, dropradius: 20, perturbance: 0.03, interactive: true, crossorigin: '' }; ripples.prototype = { // set up pointer (mouse + touch) events setuppointerevents: function() { var that = this; function pointereventsenabled() { return that.visible && that.running && that.interactive; } function dropatpointer(pointer, big) { if (pointereventsenabled()) { that.dropatpointer( pointer, that.dropradius * (big ? 1.5 : 1), (big ? 0.14 : 0.01) ); } } // start listening to pointer events this.$el // create regular, small ripples for mouse move and touch events... .on('mousemove.ripples', function(e) { dropatpointer(e); }) .on('touchmove.ripples touchstart.ripples', function(e) { var touches = e.originalevent.changedtouches; for (var i = 0; i < touches.length; i++) { dropatpointer(touches[i]); } }) // ...and only a big ripple on mouse down events. .on('mousedown.ripples', function(e) { dropatpointer(e, true); }); }, // load the image either from the options or the element's css rules. loadimage: function() { var that = this; gl = this.context; var newimagesource = this.imageurl || extracturl(this.originalcssbackgroundimage) || extracturl(this.$el.css('backgroundimage')); // if image source is unchanged, don't reload it. if (newimagesource == this.imagesource) { return; } this.imagesource = newimagesource; // falsy source means no background. if (!this.imagesource) { this.settransparenttexture(); return; } // load the texture from a new image. var image = new image; image.onload = function() { gl = that.context; // only textures with dimensions of powers of two can have repeat wrapping. function ispoweroftwo(x) { return (x & (x - 1)) == 0; } var wrapping = (ispoweroftwo(image.width) && ispoweroftwo(image.height)) ? gl.repeat : gl.clamp_to_edge; gl.bindtexture(gl.texture_2d, that.backgroundtexture); 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.backgroundwidth = image.width; that.backgroundheight = image.height; // hide the background that we're replacing. that.hidecssbackground(); }; // fall back to a transparent texture when loading the image failed. image.onerror = function() { gl = that.context; that.settransparenttexture(); }; // disable cors when the image source is a data uri. image.crossorigin = isdatauri(this.imagesource) ? null : this.crossorigin; image.src = this.imagesource; }, step: function() { gl = this.context; if (!this.visible) { return; } this.computetextureboundaries(); if (this.running) { this.update(); } 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.bindframebuffer(gl.framebuffer, null); gl.viewport(0, 0, this.canvas.width, this.canvas.height); gl.enable(gl.blend); 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.uniform1f(this.renderprogram.locations.perturbance, this.perturbance); 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(); gl.disable(gl.blend); }, update: function() { gl.viewport(0, 0, this.resolution, this.resolution); gl.bindframebuffer(gl.framebuffer, this.framebuffers[this.bufferwriteindex]); bindtexture(this.textures[this.bufferreadindex]); gl.useprogram(this.updateprogram.id); this.drawquad(); this.swapbufferindices(); }, swapbufferindices: function() { this.bufferwriteindex = 1 - this.bufferwriteindex; this.bufferreadindex = 1 - this.bufferreadindex; }, computetextureboundaries: function() { var backgroundsize = this.$el.css('background-size'); var backgroundattachment = this.$el.css('background-attachment'); var backgroundposition = translatebackgroundposition(this.$el.css('background-position')); // here the 'container' is the element which the background adapts to // (either the chrome window or some element, depending on attachment) var container; if (backgroundattachment == 'fixed') { container = { left: window.pagexoffset, top: window.pageyoffset }; container.width = $window.width(); container.height = $window.height(); } else { container = this.$el.offset(); container.width = this.$el.innerwidth(); container.height = this.$el.innerheight(); } // todo: background-clip if (backgroundsize == 'cover') { var scale = math.max(container.width / this.backgroundwidth, container.height / this.backgroundheight); var backgroundwidth = this.backgroundwidth * scale; var backgroundheight = this.backgroundheight * scale; } else if (backgroundsize == 'contain') { var scale = math.min(container.width / this.backgroundwidth, container.height / this.backgroundheight); var backgroundwidth = this.backgroundwidth * scale; var backgroundheight = this.backgroundheight * scale; } else { backgroundsize = backgroundsize.split(' '); var backgroundwidth = backgroundsize[0] || ''; var backgroundheight = backgroundsize[1] || backgroundwidth; if (ispercentage(backgroundwidth)) { backgroundwidth = container.width * parsefloat(backgroundwidth) / 100; } else if (backgroundwidth != 'auto') { backgroundwidth = parsefloat(backgroundwidth); } if (ispercentage(backgroundheight)) { backgroundheight = container.height * 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); } } } // compute backgroundx and backgroundy in page coordinates var backgroundx = backgroundposition[0]; var backgroundy = backgroundposition[1]; if (ispercentage(backgroundx)) { backgroundx = container.left + (container.width - backgroundwidth) * parsefloat(backgroundx) / 100; } else { backgroundx = container.left + parsefloat(backgroundx); } if (ispercentage(backgroundy)) { backgroundy = container.top + (container.height - backgroundheight) * parsefloat(backgroundy) / 100; } else { backgroundy = container.top + 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 = 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.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 vec2 delta;', 'uniform float perturbance;', 'varying vec2 ripplescoord;', 'varying vec2 backgroundcoord;', 'void main() {', 'float height = texture2d(samplerripples, ripplescoord).r;', 'float heightx = texture2d(samplerripples, vec2(ripplescoord.x + delta.x, ripplescoord.y)).r;', 'float heighty = texture2d(samplerripples, vec2(ripplescoord.x, ripplescoord.y + delta.y)).r;', 'vec3 dx = vec3(delta.x, heightx - height, 0.0);', 'vec3 dy = vec3(0.0, heighty - height, delta.y);', 'vec2 offset = -normalize(cross(dy, dx)).xz;', '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.uniform2fv(this.renderprogram.locations.delta, this.texturedelta); }, inittexture: function() { this.backgroundtexture = gl.createtexture(); gl.bindtexture(gl.texture_2d, this.backgroundtexture); 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); }, settransparenttexture: function() { gl.bindtexture(gl.texture_2d, this.backgroundtexture); gl.teximage2d(gl.texture_2d, 0, gl.rgba, gl.rgba, gl.unsigned_byte, transparentpixels); }, hidecssbackground: function() { // check whether we're changing inline css or overriding a global css rule. var inlinecss = this.$el[0].style.backgroundimage; if (inlinecss == 'none') { return; } this.originalinlinecss = inlinecss; this.originalcssbackgroundimage = this.$el.css('backgroundimage'); this.$el.css('backgroundimage', 'none'); }, restorecssbackground: function() { // restore background by either changing the inline css rule to what it was, or // simply remove the inline css rule if it never was inlined. this.$el.css('backgroundimage', this.originalinlinecss || ''); }, dropatpointer: function(pointer, radius, strength) { var borderleft = parseint(this.$el.css('border-left-width')) || 0, bordertop = parseint(this.$el.css('border-top-width')) || 0; this.drop( pointer.pagex - this.$el.offset().left - borderleft, pointer.pagey - this.$el.offset().top - bordertop, radius, strength ); }, /** * public methods */ drop: function(x, y, radius, strength) { gl = this.context; var elwidth = this.$el.innerwidth(); var elheight = this.$el.innerheight(); 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); gl.bindframebuffer(gl.framebuffer, this.framebuffers[this.bufferwriteindex]); bindtexture(this.textures[this.bufferreadindex]); 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(); this.swapbufferindices(); }, updatesize: function() { var newwidth = this.$el.innerwidth(), newheight = this.$el.innerheight(); if (newwidth != this.canvas.width || newheight != this.canvas.height) { this.canvas.width = newwidth; this.canvas.height = newheight; } }, destroy: function() { this.$el .off('.ripples') .removeclass('jquery-ripples') .removedata('ripples'); // make sure the last used context is garbage-collected gl = null; $(window).off('resize', this.updatesize); this.$canvas.remove(); this.restorecssbackground(); this.destroyed = true; }, show: function() { this.visible = true; this.$canvas.show(); this.hidecssbackground(); }, hide: function() { this.visible = false; this.$canvas.hide(); this.restorecssbackground(); }, pause: function() { this.running = false; }, play: function() { this.running = true; }, set: function(property, value) { switch (property) { case 'dropradius': case 'perturbance': case 'interactive': case 'crossorigin': this[property] = value; break; case 'imageurl': this.imageurl = value; this.loadimage(); break; } } }; // ripples plugin definition // ========================== var old = $.fn.ripples; $.fn.ripples = function(option) { if (!config) { throw new error('your browser does not support webgl, the oes_texture_float extension or rendering to floating point textures.'); } var args = (arguments.length > 1) ? array.prototype.slice.call(arguments, 1) : undefined; return this.each(function() { var $this = $(this), data = $this.data('ripples'), options = $.extend({}, ripples.defaults, $this.data(), typeof option == 'object' && option); if (!data && typeof option == 'string') { 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; }; })));