Source: gfx/core/renderers/webgl/managers/FilterManager.js

var WebGLManager = require('./WebGLManager'),
  RenderTarget = require('../utils/RenderTarget'),
  CONST = require('../../../../const'),
  Quad = require('../utils/Quad'),
  math = require('../../../math');

/**
 * @class
 * @extends WebGLManager
 * @param renderer {WebGLRenderer} The renderer this manager works for.
 */
function FilterManager(renderer) {
  WebGLManager.call(this, renderer);

    /**
     * @member {object[]}
     */
  this.filterStack = [];

  this.filterStack.push({
    renderTarget:renderer.currentRenderTarget,
    filter:[],
    bounds:null,
  });

    /**
     * @member {RenderTarget[]}
     */
  this.texturePool = [];

    /**
     * The size of the texture
     *
     * @member {Rectangle}
     */
    // listen for context and update necessary buffers
    // TODO make this dynamic!
    // TODO test this out by forces power of two?
  this.textureSize = new math.Rectangle(0, 0, renderer.width, renderer.height);

    /**
     * The current frame
     *
     * @member {Rectangle}
     */
  this.currentFrame = null;
}

FilterManager.prototype = Object.create(WebGLManager.prototype);
FilterManager.prototype.constructor = FilterManager;
module.exports = FilterManager;


/**
 * Called when there is a WebGL context change.
 *
 */
FilterManager.prototype.onContextChange = function() {
  this.texturePool.length = 0;

  var gl = this.renderer.gl;
  this.quad = new Quad(gl);
};

/**
 * @param renderer {WebGLRenderer}
 * @param buffer {ArrayBuffer}
 */
FilterManager.prototype.setFilterStack = function(filterStack) {
  this.filterStack = filterStack;
};

/**
 * Applies the filter and adds it to the current filter stack.
 *
 * @param target {DisplayObject}
 * @param filters {AbstractFiler[]} the filters that will be pushed to the current filter stack
 */
FilterManager.prototype.pushFilter = function(target, filters) {
    // get the bounds of the object..
    // TODO replace clone with a copy to save object creation
  var bounds = target.filterArea ? target.filterArea.clone() : target.getBounds();

    // bounds = bounds.clone();

    // round off the rectangle to get a nice smoooooooth filter :)
  bounds.x = bounds.x | 0;
  bounds.y = bounds.y | 0;
  bounds.width = bounds.width | 0;
  bounds.height = bounds.height | 0;


    // padding!
  var padding = filters[0].padding | 0;
  bounds.x -= padding;
  bounds.y -= padding;
  bounds.width += padding * 2;
  bounds.height += padding * 2;


  if (this.renderer.currentRenderTarget.transform) {
        // TODO this will break if the renderTexture transform is anything other than a translation.
        // Will need to take the full matrix transform into acount..
    var transform = this.renderer.currentRenderTarget.transform;

    bounds.x += transform.tx;
    bounds.y += transform.ty;

    this.capFilterArea(bounds);

    bounds.x -= transform.tx;
    bounds.y -= transform.ty;
  }
  else {
    this.capFilterArea(bounds);
  }

  if (bounds.width > 0 && bounds.height > 0) {
    this.currentFrame = bounds;

    var texture = this.getRenderTarget();

    this.renderer.setRenderTarget(texture);

        // clear the texture..
    texture.clear();

        // TODO get rid of object creation!
    this.filterStack.push({
      renderTarget: texture,
      filter: filters,
    });

  }
  else {
        // push somthing on to the stack that is empty
    this.filterStack.push({
      renderTarget: null,
      filter: filters,
    });
  }
};


/**
 * Removes the last filter from the filter stack and returns it.
 *
 */
FilterManager.prototype.popFilter = function() {
  var filterData = this.filterStack.pop();
  var previousFilterData = this.filterStack[this.filterStack.length - 1];

  var input = filterData.renderTarget;

    // if the renderTarget is null then we don't apply the filter as its offscreen
  if (!filterData.renderTarget) {
    return;
  }

  var output = previousFilterData.renderTarget;

    // use program
  var gl = this.renderer.gl;


  this.currentFrame = input.frame;

  this.quad.map(this.textureSize, input.frame);


    // TODO.. this probably only needs to be done once!
  gl.bindBuffer(gl.ARRAY_BUFFER, this.quad.vertexBuffer);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quad.indexBuffer);

  var filters = filterData.filter;

    // assuming all filters follow the correct format??
  gl.vertexAttribPointer(this.renderer.shaderManager.defaultShader.attributes.aVertexPosition, 2, gl.FLOAT, false, 0, 0);
  gl.vertexAttribPointer(this.renderer.shaderManager.defaultShader.attributes.aTextureCoord, 2, gl.FLOAT, false, 0, 2 * 4 * 4);
  gl.vertexAttribPointer(this.renderer.shaderManager.defaultShader.attributes.aColor, 4, gl.FLOAT, false, 0, 4 * 4 * 4);

    // restore the normal blendmode!
  this.renderer.blendModeManager.setBlendMode(CONST.BLEND_MODES.NORMAL);

  if (filters.length === 1) {
        // TODO (cengler) - There has to be a better way then setting this each time?
    if (filters[0].uniforms.dimensions) {
      filters[0].uniforms.dimensions.value[0] = this.renderer.width;
      filters[0].uniforms.dimensions.value[1] = this.renderer.height;
      filters[0].uniforms.dimensions.value[2] = this.quad.vertices[0];
      filters[0].uniforms.dimensions.value[3] = this.quad.vertices[5];
    }

    filters[0].applyFilter(this.renderer, input, output);
    this.returnRenderTarget(input);

  }
  else {
    var flipTexture = input;
    var flopTexture = this.getRenderTarget(true);

    for (var i = 0; i < filters.length - 1; i++) {
      var filter = filters[i];

            // TODO (cengler) - There has to be a better way then setting this each time?
      if (filter.uniforms.dimensions) {
        filter.uniforms.dimensions.value[0] = this.renderer.width;
        filter.uniforms.dimensions.value[1] = this.renderer.height;
        filter.uniforms.dimensions.value[2] = this.quad.vertices[0];
        filter.uniforms.dimensions.value[3] = this.quad.vertices[5];
      }

      filter.applyFilter(this.renderer, flipTexture, flopTexture);

      var temp = flipTexture;
      flipTexture = flopTexture;
      flopTexture = temp;
    }

    filters[filters.length - 1].applyFilter(this.renderer, flipTexture, output);

    this.returnRenderTarget(flipTexture);
    this.returnRenderTarget(flopTexture);
  }

  return filterData.filter;
};

/**
 * Grabs an render target from the internal pool
 *
 * @param clear {boolean} Whether or not we need to clear the RenderTarget
 * @return {RenderTarget}
 */
FilterManager.prototype.getRenderTarget = function(clear) {
  var renderTarget = this.texturePool.pop() || new RenderTarget(this.renderer.gl, this.textureSize.width, this.textureSize.height, CONST.SCALE_MODES.LINEAR, this.renderer.resolution * CONST.FILTER_RESOLUTION);
  renderTarget.frame = this.currentFrame;

  if (clear) {
    renderTarget.clear(true);
  }

  return renderTarget;
};

/*
 * Returns a RenderTarget to the internal pool
 * @param renderTarget {RenderTarget} The RenderTarget we want to return to the pool
 */
FilterManager.prototype.returnRenderTarget = function(renderTarget) {
  this.texturePool.push(renderTarget);
};

/*
 * Applies the filter
 * @param shader {Shader} The shader to upload
 * @param inputTarget {RenderTarget}
 * @param outputTarget {RenderTarget}
 * @param clear {boolean} Whether or not we want to clear the outputTarget
 */
FilterManager.prototype.applyFilter = function(shader, inputTarget, outputTarget, clear) {
  var gl = this.renderer.gl;

  this.renderer.setRenderTarget(outputTarget);

  if (clear) {
    outputTarget.clear();
  }

    // set the shader
  this.renderer.shaderManager.setShader(shader);

    // TODO (cengler) - Can this be cached and not `toArray`ed each frame?
  shader.uniforms.projectionMatrix.value = this.renderer.currentRenderTarget.projectionMatrix.toArray(true);

    // TODO can this be optimised?
  shader.syncUniforms();
/*
    gl.vertexAttribPointer(shader.attributes.aVertexPosition, 2, gl.FLOAT, false, 0, 0);
    gl.vertexAttribPointer(shader.attributes.aTextureCoord, 2, gl.FLOAT, false, 0, 2 * 4 * 4);
    gl.vertexAttribPointer(shader.attributes.aColor, 4, gl.FLOAT, false, 0, 4 * 4 * 4);
*/

  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, inputTarget.texture);

  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
  this.renderer.drawCount++;
};

/*
 * Calculates the mapped matrix
 * @param filterArea {Rectangle} The filter area
 * @param sprite {Sprite} the target sprite
 * @param outputMatrix {Matrix} @alvin
 */
// TODO playing around here.. this is temporary - (will end up in the shader)
FilterManager.prototype.calculateMappedMatrix = function(filterArea, sprite, outputMatrix) {
  var worldTransform = sprite.worldTransform.copy(math.Matrix.TEMP_MATRIX),
    texture = sprite._texture.baseTexture;

  var mappedMatrix = outputMatrix.identity();

    // scale..
  var ratio = this.textureSize.height / this.textureSize.width;

  mappedMatrix.translate(filterArea.x / this.textureSize.width, filterArea.y / this.textureSize.height);

  mappedMatrix.scale(1 , ratio);

  var translateScaleX = (this.textureSize.width / texture.width);
  var translateScaleY = (this.textureSize.height / texture.height);

  worldTransform.tx /= texture.width * translateScaleX;
  worldTransform.ty /= texture.width * translateScaleX;

  worldTransform.invert();

  mappedMatrix.prepend(worldTransform);

    // apply inverse scale..
  mappedMatrix.scale(1 , 1 / ratio);

  mappedMatrix.scale(translateScaleX , translateScaleY);

  mappedMatrix.translate(sprite.anchor.x, sprite.anchor.y);

  return mappedMatrix;

    // Keeping the orginal as a reminder to me on how this works!
    //
    // var m = new math.Matrix();

    // // scale..
    // var ratio = this.textureSize.height / this.textureSize.width;

    // m.translate(filterArea.x / this.textureSize.width, filterArea.y / this.textureSize.height);


    // m.scale(1 , ratio);


    // var transform = wt.clone();

    // var translateScaleX = (this.textureSize.width / 620);
    // var translateScaleY = (this.textureSize.height / 380);

    // transform.tx /= 620 * translateScaleX;
    // transform.ty /= 620 * translateScaleX;

    // transform.invert();

    // transform.append(m);

    // // apply inverse scale..
    // transform.scale(1 , 1/ratio);

    // transform.scale( translateScaleX , translateScaleY );

    // return transform;
};

/*
 * Constrains the filter area to the texture size
 * @param filterArea {Rectangle} The filter area we want to cap
 */
FilterManager.prototype.capFilterArea = function(filterArea) {
  if (filterArea.x < 0) {
    filterArea.width += filterArea.x;
    filterArea.x = 0;
  }

  if (filterArea.y < 0) {
    filterArea.height += filterArea.y;
    filterArea.y = 0;
  }

  if (filterArea.x + filterArea.width > this.textureSize.width) {
    filterArea.width = this.textureSize.width - filterArea.x;
  }

  if (filterArea.y + filterArea.height > this.textureSize.height) {
    filterArea.height = this.textureSize.height - filterArea.y;
  }
};

/*
 * Resizes all the render targets in the pool
 * @param width {number} the new width
 * @param height {number} the new height
 */
FilterManager.prototype.resize = function(width, height) {
  this.textureSize.width = width;
  this.textureSize.height = height;

  for (var i = 0; i < this.texturePool.length; i++) {
    this.texturePool[i].resize(width, height);
  }
};

/**
 * Destroys the filter and removes it from the filter stack.
 *
 */
FilterManager.prototype.destroy = function() {
  this.quad.destroy();

  WebGLManager.prototype.destroy.call(this);

  this.filterStack = null;
  this.offsetY = 0;

    // destroy textures
  for (var i = 0; i < this.texturePool.length; i++) {
    this.texturePool[i].destroy();
  }

  this.texturePool = null;
};