Source: device/screen/gl2d.js

/**
 * Mobizen - Mobizen/Device/Screen/GL2D
 *
 * IMPORTANT NOTE: This file is licensed only for use in providing the RSUPPORT services,
 *
 * @copyright 2015 RSUPPORT CO., LTD. All rights reserved.
 * @license Dedicated to the public domain.
 * @author Joon Kyoung <firejune@gmail.com>
 *
 */

(function(exports) {
  "use strict";

  /** @typedef {Mobizen} */
  var Mobizen = exports.Mobizen || (exports.Mobizen = {})
  /** @typedef {Mobizen.Device} */
    , Device = Mobizen.Device || (Mobizen.Device = {});

  /**
   * @constructs GL2D
   * @memberof Mobizen.Device.Screen
   * @param {Mobizen.Device.Screen.View#canvas} canvas
   *
   */
  function GL2D(canvas) {
    console.info('Screen.GL2D');

    /** @type {string} */
    this.effect = 'normal';
    /** @typedef {Mobizen.Device.Screen.View#canvas|Mobizen.Device.Screen.View#_canvas} */
    this.canvas = canvas;

    // Store getContext function for later use
    canvas.$getContext = canvas.getContext;

    /**
     * Override getContext function with gl2d enabled version
     * @return {object} gl2d
     * @override
     */
    canvas.getContext = function() {
      try {
        /**
         * Get A WebGL context
         * @typedef {WebGLContext}
         */
        this.gl = getWebGLContext(canvas);

        createTexture(this.gl);
        setupProgram(this.gl, canvas, this.effect);

        return this;
      } catch(e) {
        console.error(e);
      }

      return canvas.$getContext('2d');
    }.bind(this);
  }

  GL2D.prototype = /** @lends Mobizen.Device.Screen.GL2D# */ {
    /**
     * @param {string} effect
     * @return {boolean}
     */
    resizeCanvas: function(effect) {
      if (!this.gl) {
        return false;
      }

      if (effect && kernels[effect]) {
        this.effect = effect;
      }

      //console.log('GL2D.resizeCanvas', this.effect);

      // Set the resolution
      this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
      setupProgram(this.gl, this.canvas, this.effect);

      return true;
    },

    /**
     * @param {HTMLElement} image
     * @param {number} x
     * @param {number} y
     * @param {number} width
     * @param {number} height
     */
    drawImage: function(image, x, y, width, height) {
      var gl = this.gl
        , x1 = x
        , x2 = x + width
        , y1 = y
        , y2 = y + height;

      // Upload the image into the texture.
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

      // Set a rectangle the same size as the image.
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
         x1, y1,
         x2, y1,
         x1, y2,
         x1, y2,
         x2, y1,
         x2, y2]), gl.STATIC_DRAW);

      // Draw the rectangle.
      gl.drawArrays(gl.TRIANGLES, 0, 6);
    },

    /**
     * @param {number} x
     * @param {number} y
     * @param {number} width
     * @param {number} height
     * @return {object} raw image data
     */
    getImageData: function(x, y, width, height) {
      var gl = this.gl
        , w = width * 4
        , h = height
        , data = tempCtx.createImageData(width, height)
        , buffer = new Uint8Array(width * height * 4);

      gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer);

      for (var i = 0, maxI = h / 2; i < maxI; ++i) {
        for (var j = 0, maxJ = w; j < maxJ; ++j) {
          var index1 = i * w + j
            , index2 = (h - i - 1) * w + j;

          data.data[index1] = buffer[index2];
          data.data[index2] = buffer[index1];
        }
      }

      return data;
    },

    /**
     * @param {HTMLElement} image
     * @param {number} x
     * @param {number} y
     */
    putImageData: function(image, x, y) {
      this.drawImage(image, x, y, image.width, image.height);
    },

    /**
     * @virtual
     * @param {number} x
     * @param {number} y
     */
    scale: function(x, y) {
      console.warn('GL2D.scale', 'NOT IMPLEMENTED', x, y);
    }
  };

  var tempCanvas = document.createElement('canvas');
  var tempCtx = tempCanvas.getContext('2d');

  /**
   * Define several convolution kernels
   * @memberof Mobizen.Device.Screen.GL2D~
   * @typedef {kernels}
   * @prop {array} normal - matrix
   * @prop {array} gaussianBlur - matrix
   * @prop {array} gaussianBlur2 - matrix
   * @prop {array} gaussianBlur3 - matrix
   * @prop {array} unsharpen - matrix
   * @prop {array} sharpness - matrix
   * @prop {array} sharpen - matrix
   * @prop {array} edgeDetect - matrix
   * @prop {array} edgeDetect2 - matrix
   * @prop {array} edgeDetect3 - matrix
   * @prop {array} edgeDetect4 - matrix
   * @prop {array} edgeDetect5 - matrix
   * @prop {array} edgeDetect6 - matrix
   * @prop {array} sobelHorizontal - matrix
   * @prop {array} sobelVertical - matrix
   * @prop {array} previtHorizontal - matrix
   * @prop {array} previtVertical - matrix
   * @prop {array} boxBlur - matrix
   * @prop {array} triangleBlur - matrix
   * @prop {array} emboss - matrix
   */
  var kernels = {
    normal:           [0, 0, 0, 0, 1, 0, 0, 0, 0],
    gaussianBlur:     [0.045, 0.122, 0.045, 0.122, 0.332, 0.122, 0.045, 0.122, 0.045],
    gaussianBlur2:    [1, 2, 1, 2, 4, 2, 1, 2, 1],
    gaussianBlur3:    [0, 1, 0, 1, 1, 1, 0, 1, 0],
    unsharpen:        [-1, -1, -1, -1, 9, -1, -1, -1, -1],
    sharpness:        [0,-1, 0, -1, 5,-1, 0,-1, 0],
    sharpen:          [-1, -1, -1, -1, 16, -1, -1, -1, -1],
    edgeDetect:       [-0.125, -0.125, -0.125, -0.125, 1, -0.125, -0.125, -0.125, -0.125],
    edgeDetect2:      [-1    , -1, -1, -1, 8, -1, -1, -1, -1 ],
    edgeDetect3:      [-5, 0, 0, 0, 0, 0, 0, 0, 5],
    edgeDetect4:      [-1, -1, -1, 0, 0, 0, 1, 1, 1],
    edgeDetect5:      [-1, -1, -1, 2, 2, 2, -1, -1, -1],
    edgeDetect6:      [-5, -5, -5, -5, 39, -5, -5, -5, -5],
    sobelHorizontal:  [1, 2, 1, 0, 0, 0, -1, -2, -1],
    sobelVertical:    [1, 0, -1, 2, 0, -2, 1, 0, -1],
    previtHorizontal: [1, 1, 1, 0, 0, 0, -1, -1, -1],
    previtVertical:   [1, 0, -1, 1, 0, -1, 1, 0, -1],
    boxBlur:          [0.111, 0.111, 0.111, 0.111, 0.111, 0.111, 0.111, 0.111, 0.111],
    triangleBlur:     [0.0625, 0.125, 0.0625, 0.125, 0.25, 0.125, 0.0625, 0.125, 0.0625],
    emboss:           [-2, -1, 0, -1, 1, 1, 0, 1, 2]
  };

  /**
   * @memberof Mobizen.Device.Screen.GL2D~
   * @param {WebGLContext} gl
   * @param {HTMLElement} canvas
   * @param {string} effect
   * @return {object} program
   */
  function setupProgram(gl, canvas, effect) {
    // setup GLSL program
    var vertexShader = createShaderScript(gl, "2d-vertex-shader")
      , fragmentShader = createShaderScript(gl, "2d-fragment-shader")
      , program = loadProgram(gl, [vertexShader, fragmentShader])

      // look up where the vertex data needs to go.
      , positionLocation = gl.getAttribLocation(program, "a_position")
      , texCoordLocation = gl.getAttribLocation(program, "a_texCoord")

      // lookup uniforms
      , resolutionLocation = gl.getUniformLocation(program, "u_resolution")
      , textureSizeLocation = gl.getUniformLocation(program, "u_textureSize")
      , kernelLocation = gl.getUniformLocation(program, "u_kernel[0]")
      , flipYLocation = gl.getUniformLocation(program, "u_flipY")

      , texCoordBuffer = gl.createBuffer()
      , positionBuffer = gl.createBuffer();

    gl.useProgram(program);

    // provide texture coordinates for the rectangle.
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        0.0, 0.0,
        1.0, 0.0,
        0.0, 1.0,
        0.0, 1.0,
        1.0, 0.0,
        1.0, 1.0]), gl.STATIC_DRAW);

    gl.enableVertexAttribArray(texCoordLocation);
    gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

    // set the resolution
    gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

    // set the size of the image
    gl.uniform2f(textureSizeLocation, canvas.width, canvas.height);

    // finally draw the result to the canvas.
    gl.uniform1f(flipYLocation, -1);  // need to y flip for canvas

    // set the kernel
    gl.uniform1fv(kernelLocation, kernels[effect || 'normal']);

    // Create a buffer for the position of the rectangle corners.
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

    return program;
  }

  /**
   * @memberof Mobizen.Device.Screen.GL2D~
   * @param {WebGLContext} gl
   */
  function createTexture(gl) {
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // Set up texture so we can render any size image and so we are
    // working with pixels.
    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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

    return texture;
  }

  /**
   * Creates a webgl context. If creation fails it will
   * change the contents of the container of the <canvas>
   * tag to an error message with the correct links for WebGL.
   *
   * @memberof Mobizen.Device.Screen.GL2D~
   * @param {HTMLElement} canvas - The canvas element to create a context from.
   * @return {WebGLRenderingContext} The created context.
   * @exception Will throw an error if the browser is not suport WebGL.
   */
  function getWebGLContext(canvas) {
    if (!exports.WebGLRenderingContext) {
      throw "This browser dose not supports WebGL";
    }

    var context = create3DContext(canvas);
    if (!context) {
      throw "It doesn't appear your computer can support WebGL.";
    }

    return context;
  }

  /**
   * Creates a webgl context.
   *
   * @memberof Mobizen.Device.Screen.GL2D~
   * @param {!Canvas} canvas - The canvas tag to get context from. If one is not passed in one will be created.
   * @return {!WebGLContext} The created context.
   */
  function create3DContext(canvas) {
    var names = ["webgl", "experimental-webgl"]
      , context = null;

    for (var i = 0; i < names.length; ++i) {
      try {
        context = canvas.$getContext(names[i]);
      } catch(e) {}

      if (context) {
        break;
      }
    }

    return context;
  }

  /**
   * Loads a shader.
   *
   * @memberof Mobizen.Device.Screen.GL2D~
   * @param {!WebGLContext} gl The WebGLContext to use.
   * @param {string} shader source The shader source.
   * @param {number} shader type The type of shader.
   * @return {!WebGLShader} The created shader.
   * @exception Will throw an error if the shader is not compiled.
   */
  function loadShader(gl, source, type) {
    // Create the shader object
    var shader = gl.createShader(type);

    // Load the shader source
    gl.shaderSource(shader, source);

    // Compile the shader
    gl.compileShader(shader);

    // Check the compile status
    var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!compiled) {
      // Something went wrong during compilation; get the error
      var error = gl.getShaderInfoLog(shader);
      gl.deleteShader(shader);

      throw "Shader compile error '" + shader + "':" + error;
    }

    return shader;
  }

  /**
   * Creates a program, attaches shaders, binds attrib locations, links the
   * program and calls useProgram.
   *
   * @memberof Mobizen.Device.Screen.GL2D~
   * @param {!WebGLContext} gl The WebGLContext to use.
   * @param {!Array.<!WebGLShader>} shaders 0 The shaders to attach
   * @return {program}
   * @exception Will throw an error if the program is not linked.
   */
  function loadProgram(gl, shaders) {
    var program = gl.createProgram();
    for (var i = 0; i < shaders.length; ++i) {
      gl.attachShader(program, shaders[i]);
    }

    gl.linkProgram(program);

    // Check the link status
    var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (!linked) {
      // something went wrong with the link
      var error = gl.getProgramInfoLog (program);
      gl.deleteProgram(program);

      throw "Error in program linking:" + error;
    }

    return program;
  }

  /**
   * Loads a shader from a script.
   *
   * @memberof Mobizen.Device.Screen.GL2D~
   * @param {!WebGLContext} gl The WebGLContext to use.
   * @param {string} script id The id of the script object.
   * @return {!WebGLShader} The created shader.
   * @exception Will throw an error if the id argument is not accepted.
   */
  function createShaderScript(gl, id) {
    var script = {
        "2d-vertex-shader": {
          type: "x-shader/x-vertex",
          code: gl.VERTEX_SHADER,
          text: [
            "attribute vec2 a_position;",
            "attribute vec2 a_texCoord;",

            "uniform vec2 u_resolution;",
            "uniform float u_flipY;",
            "varying vec2 v_texCoord;",

            "void main() {",
               // convert the rectangle from pixels to 0.0 to 1.0
               "vec2 zeroToOne = a_position / u_resolution;",

               // convert from 0->1 to 0->2
               "vec2 zeroToTwo = zeroToOne * 2.0;",

               // convert from 0->2 to -1->+1 (clipspace)
               "vec2 clipSpace = zeroToTwo - 1.0;",

               "gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);",

               // pass the texCoord to the fragment shader
               // The GPU will interpolate this value between points.
               "v_texCoord = a_texCoord;",
            "}"
          ].join("\n")
        },

        "2d-fragment-shader": {
          type: "x-shader/x-fragment",
          code: gl.FRAGMENT_SHADER,
          text: [
            "precision mediump float;",

            // texture image
            "uniform sampler2D u_image;",
            "uniform vec2 u_textureSize;",
            "uniform float u_kernel[9];",

            // the texCoords passed in from the vertex shader.
            "varying vec2 v_texCoord;",

            "void main() {",
              "vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;",

              "vec4 colorSum =",
                "texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +",
                "texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +",
                "texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +",
                "texture2D(u_image, v_texCoord + onePixel * vec2(-1,  0)) * u_kernel[3] +",
                "texture2D(u_image, v_texCoord + onePixel * vec2( 0,  0)) * u_kernel[4] +",
                "texture2D(u_image, v_texCoord + onePixel * vec2( 1,  0)) * u_kernel[5] +",
                "texture2D(u_image, v_texCoord + onePixel * vec2(-1,  1)) * u_kernel[6] +",
                "texture2D(u_image, v_texCoord + onePixel * vec2( 0,  1)) * u_kernel[7] +",
                "texture2D(u_image, v_texCoord + onePixel * vec2( 1,  1)) * u_kernel[8] ;",

              "float kernelWeight =",
                "u_kernel[0] +",
                "u_kernel[1] +",
                "u_kernel[2] +",
                "u_kernel[3] +",
                "u_kernel[4] +",
                "u_kernel[5] +",
                "u_kernel[6] +",
                "u_kernel[7] +",
                "u_kernel[8] ;",

              "if (kernelWeight <= 0.0) {",
                "kernelWeight = 1.0;",
              "}",

              // Look up a color from the texture.
              "gl_FragColor = vec4((colorSum / kernelWeight).rgb, 1);",
            "}"
          ].join("\n")
        }
      }[id];

    if (!script) {
      throw "Unknown script" + id;
    }

    return loadShader(gl, script.text, script.code);
  }


  /*
   * Exporting modules
   */

  Device.Screen.GL2D = GL2D;

})(this);
comments powered by Disqus