javascript:(function(){
  /* http://d.hatena.ne.jp/edvakf/20090501/1241208337 */

  var Ripple = {
    COLOR : [0,0,255],    /* color in RGB */
    R : 100,              /* ripple radius */
    T : 50,               /* animation timesteps */
    HALF_OSCIL_NUM : 4,   /* number of half oscillations in T / angular velocity = HALF_OSCIL_NUM*PI/T */
    RESOLUTION : 32,      /* number of ColorStops to form gradient */

    ripplets : [],
    timer : null,
    cache : {},

    getRGBA : function(r,g,b,a){
      return 'rgba(' + r + ',' + g + ',' + b + ',' + a +')';
    },

    /* ripplet generating function : time -> height */
    generator : function(t){
      return Math.pow(Math.sin(t*this.HALF_OSCIL_NUM*Math.PI/this.T),2).toFixed(2);
    },

    /* height of ripplet : (distance, time) -> height */
    dampedHeight : function(r,t){
      var R = this.R, T = this.T;
      if (r > t*R/T) return 0;
      return this.generator(t-T*r/R) * (R-r)/R * (1-t/T);
    },

    /* may be replaced by custom function depending on color scheme of your choice */
    height2color : function(h){
      return this.getRGBA(this.COLOR[0],this.COLOR[1],this.COLOR[2],h);
    },

    /* get height and convert to color : (distance, time) -> color */
    getColorAt : function(r,t){
      return this.height2color(this.dampedHeight(r,t));
    },

    /* set gradients for a state of ripple : (CanvasGradient,time) -> null */
    setGradient : function(grad,t){
      var num = this.RESOLUTION - 1;
      for (var i=0;i<=num;i++) {
        grad.addColorStop( i/num , this.getColorAt(this.R*i/num,t) );
      }
    },

    /* start and/or continue animation */
    animate : function(){
      if (this.timer) {
        var n = this.ripplets.length;
        while (n--) {
          var ripplet = this.ripplets[n];
          ripplet.evolve();
          if (ripplet.t == this.T) {
            ripplet.finalize();
            this.ripplets.splice(n,1);
          }
        }
        if (!this.ripplets.length) {
          clearInterval(this.timer);
          this.timer = null;
        }
      } else {
        var self = this;
        this.timer = setInterval(function(){self.animate()},50);
      }
    },

    /* add new Ripplet instance to the stack */
    setRipplet : function(x,y){
      this.ripplets.push(new this.Ripplet(x,y));
      this.animate();
    },

    finalize : function(){
      this.ripplets = [];
      this.timer = null;
      for(var x in this.cache) if(this.cache.hasOwnProperty(x)){
        delete x;
      }
      this.cache = {};
    }

  };

  /* Ripplet class */
  Ripple.Ripplet = function(x,y){
    var R = Ripple.R;
    this.x = x;
    this.y = y;
    this.t = 0;
    var canvas = this.canvas = document.createElement('canvas');
    document.body.appendChild(canvas);
    canvas.style.position = 'fixed';
    canvas.style.left = x - R + 'px';
    canvas.style.top = y - R + 'px';
    canvas.width = R*2;
    canvas.height = R*2;
    this.ctx = canvas.getContext('2d');
    this.ctx.clearRect(0,0,2*R,2*R);
  };

  Ripple.Ripplet.prototype = {
    evolve : function(){
      var ctx = this.ctx;
      var R = Ripple.R;
      ctx.clearRect(0,0,2*R,2*R);
      if (Ripple.cache[this.t]) {
        ctx.drawImage(Ripple.cache[this.t], 0, 0);
      } else {
        var R = Ripple.R;
        ctx.beginPath();
        var grad = ctx.createRadialGradient(R,R,0,R,R,R);
        Ripple.setGradient(grad,this.t);
        ctx.fillStyle = grad;
        ctx.rect(0,0,2*R,2*R);
        ctx.fill();

        var canvas2 = document.createElement('canvas');
        canvas2.height = this.canvas.height;
        canvas2.width = this.canvas.width;
        var ctx2 = canvas2.getContext('2d');
        ctx2.drawImage(this.canvas,0,0);
        Ripple.cache[this.t] = canvas2;
      }
      this.t++;
    },

    finalize : function(){
      document.body.removeChild(this.canvas);
      delete this.canvas;
      delete this;
    }
  };

  /* mouse object */
  Ripple.mouse = {
    timer : null,
    x : null,
    y : null,
    isDown : false,

    delegate : function(event){
      if (!event) event = window.event;
      Ripple.mouse[event.type](event);
    },
    
    mousedown : function(event){
      if(event.preventDefault){
        event.preventDefault();
      } else {
        event.returnValue = false;
      }
      this.x = event.clientX;
      this.y = event.clientY;
      this.isDown = true;
      Ripple.setRipplet(this.x,this.y);

      /* set timer for mousemove */
      var self = this;
      this.timer = setInterval(function(){
        if(self.isDown) Ripple.setRipplet(self.x,self.y);
      },200);
    },

    mouseup : function(){
      this.isDown = false;
      if (this.timer){
        clearInterval(this.timer);
        this.timer = null;
      }
    },
    
    mousemove : function(event){
      if(this.isDown) {
        this.x = event.clientX;
        this.y = event.clientY;
      }
    }
  };

  function listen(event, handler){
    if (window.addEventListener) {
      window.addEventListener(event,handler,false);
    } else if (window.attachEvent) {
      window.attachEvent('on'+event,handler);
    } else {
      window['on'+event] = handler;
    }
  };
  listen('mousedown',Ripple.mouse.delegate);
  listen('mouseup',Ripple.mouse.delegate);
  listen('mousemove',Ripple.mouse.delegate);

})();
