/** * jquery ripples plugin v0.1.0 / http://github.com/sirxemic/jquery.ripples * mit license * @author sirxemic / http://sirxemic.com/ */ +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(''); // 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 = { initrandomripples: function(numripples) { for (var i = 0; i < numripples; i++) { // 生成随机的波纹参数 var centerx = math.random() * this.canvas.width; var centery = math.random() * this.canvas.height; var radius = 20 + math.random() * 30; // 波纹半径在20到50之间 // 模拟鼠标点击以产生波纹 this.dropatmouse({ offsetx: centerx, offsety: centery }, radius, 0.1); // 这里的0.1是波纹的强度,可以根据需要调整 } }, 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); } } // compute backgroundx and backgroundy in page coordinates var backgroundx = backgroundposition[0]; var backgroundy = backgroundposition[1]; 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 == '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) { var that = this; gl = this.context; e.offsetx = e.offsetx || (e.pagex - this.$el.offset().left); e.offsety = e.offsety || (e.pagey - this.$el.offset().top); var elwidth = this.$el.outerwidth(); var elheight = this.$el.outerheight(); var longestside = math.max(elwidth, elheight); radius = radius / longestside; var dropposition = new float32array([ (2 * e.offsetx - elwidth) / longestside, (elheight - 2 * e.offsety) / 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.'); 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') data[option](); }); } $.fn.ripples.constructor = ripples; // ripples no conflict // ==================== $.fn.ripples.noconflict = function() { $.fn.ripples = old; return this; } }(window.jquery);