Edit in JSFiddle

<!DOCTYPE html>
<html>
<head>
<title></title>
<!-- <script src="lib/gl-matrix.js"></script> -->
</head>
<body>
<canvas id="wu" style="border: none;" width="640" height="640"></canvas>

<script id="vshader" type="x-shader/x-fragment">

  attribute vec2 aVertexPosition;

  void main() {
    gl_Position = vec4(aVertexPosition, 0, 1);
  }

</script>

<script id="fshader" type="x-shader/x-fragment">
  // setup precision for float type
  #ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
  #else
    precision mediump float;
  #endif
  precision mediump int;
  
  uniform vec2 uCanvasSize;

  // user controlled offset
  uniform vec2 uOffset;
  // user controlled scale
  uniform float uScale;

  vec4 calc(vec2 texCoord){
    float x = 0.0; 
    float y = 0.0;

    // f(x) = x^2 + c
    // let c = x + yi, a complex number. i is the imaginary number. x and y is components of texture coordinate
    // f(c) = (x + yi)^2 + x + yi = x^2 + 2*x*y*i - y^2 + x + yi 
    //      = (x^2 - y^2 + x) + (2*x*y + y)i
    // plot the function to the complex plane( just a 2D plane, whose x axis is the real number part, y axis is imaginery part of the complex number)

    for(int i=0; i<400; ++i){
      float tempX = x*x - y*y + texCoord.x;
      y = 2.0*x*y + texCoord.y;
      x = tempX;

      // if the plot x + yi radius is greater than 2(it can be arbitrary number greater or equal to 4.0, otherwise the plots will be 'chopped').
      // If it is greater than 2, the f(x) = x^2 + c could grow exponentially, therefore it is not in the Mandelbrot set.
      // we can colour it depending on the speed of growth
      if(x*x+y*y >= 16.0) {
        // float d = float(i)/50.0;
        float d = (float(i) - (log(log(sqrt(x*x+y*y))) / log(2.0))) / 50.0;

        // just a kind of green colour
        return vec4(d/1.6,d,d/2.3, 1);
      }
    }

    return vec4(0, 0, 0, 1);
  }

  void main() {
    vec2 texCoord = (gl_FragCoord.xy / uCanvasSize.xy) * 2.0 - vec2(1.0, 1.0);
    texCoord = texCoord * uScale + uOffset;


    gl_FragColor = calc(texCoord);
  }
  
</script>

<script>
var canvas, gl;
var program;
// create vertices array information.
var vertices = [
  -1, -1,
   1, -1,
  -1,  1,
   1,  1
];

// user controlled offset
var offset = [0, 0];
// user controlled scale
var scale = 1.35;


function initWebGL(){
  canvas = document.getElementById("wu");
  gl = canvas.getContext("experimental-webgl") || canvas.getContext("webgl");

  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.enable(gl.CULL_FACE);
}

function createShader(str, type) {
  var shader = gl.createShader(type);
  gl.shaderSource(shader, str);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    throw gl.getShaderInfoLog(shader);
  }

  return shader;
}

function createProgram(vstr, fstr) {
  var prog = gl.createProgram();
  var vshader = createShader(vstr, gl.VERTEX_SHADER);
  var fshader = createShader(fstr, gl.FRAGMENT_SHADER);
  gl.attachShader(prog, vshader);
  gl.attachShader(prog, fshader);

  // in OpenGL, glLinkProgram links the program object specified by program. If any shader objects of type GL_VERTEX_SHADER are attached to program, they will be used to create an executable that will run on the programmable vertex processor.
  // Since we have two different shaders that need to work together the link step is needed to verify that they actually match up. If the vertex shader is passing data on to the fragment shader the link step makes sure the fragment shader is actually accepting that input. On some devices the actual compilation may be deferred until the link step. Akin to how it works when writing a program in C, in WebGL the shader source first gets compiled into an intermediate representation and then linked to a program. In C the source gets compiled into object files and then finally the different object files get linked into an executable.
  //
  // My understanding: check whether 2 shaders match up, link shaders to the program executable.
  gl.linkProgram(prog);

  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
    throw gl.getProgramInfoLog(prog);
  }

  return prog;
}

function initShaders(){
  // shaders
  var vs = document.getElementById('vshader').textContent;
  var fs = document.getElementById('fshader').textContent;
  // shader program
  program = createProgram(vs, fs);
  // glUseProgram installs the program object specified by program as part of current rendering state
  //
  // my understanding: rendering will use this program to process vertices and fragments.
  gl.useProgram(program);

  // get the shader input vertex position variable 'pos' memory address.
  // So later we can use: gl.vertexAttribPointer to link the shader input and buffer object. shader program will
  // be able read the buffer object's vertex data into the 'pos'.
  //
  // my understanding: store shader input variable location, so we can link buffer object to the shader input.
  program.vertexPosAttrib = gl.getAttribLocation(program, 'aVertexPosition');

  program.canvasSizeUniform = gl.getUniformLocation(program, 'uCanvasSize');
  program.offsetUniform = gl.getUniformLocation(program, 'uOffset');
  program.scaleUniform = gl.getUniformLocation(program, 'uScale');
}

function drawScene(){
  // create a webgl buffer obect(vertex buffer object, in video card), like glGenBuffers(1, vertexBufferObject) in OpenGL
  var vertexBufferObject = gl.createBuffer();
  // object must be bound to the context in order for them to do anything.
  // bind the buffer object to the ARRAY_BUFFER target.
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferObject);

  // perform 2 operations:
  // 1. allocate memory for the buffer currently bound to the GL_ARRAY_BUFFER in the video card memory.
  // 2. copy vertices data into buffer object.
  //
  // my understanding: allocate memroy, upload vertices memroy from browser to video card.
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

  // tell shader program to read the vertice data from the buffer
  //
  // my understanding: link the buffer object vertex information to the vertex shader's input variable.
  // 3 means 3 components in the vertices array: x, y, z
  gl.vertexAttribPointer(program.vertexPosAttrib, 2, gl.FLOAT, false, 0, 0);
  // Without the call to gl.enableVertexAttribArray, calling gl.vertexAttribPointer on that attribute index would not mean much. The enable call does not have to be called before the vertex attribute pointer call, but it does need to be called before rendering. If the attribute is not enabled, it will not be used during rendering.
  //
  // my understanding: YOU HAVE TO enabled the shader attribute in order to accept the input during rendering time!!!!!!
  gl.enableVertexAttribArray(program.vertexPosAttrib);

  // set shader uniform data
  gl.uniform2f(program.canvasSizeUniform, canvas.width, canvas.height);

  // set offset
  gl.uniform2f(program.offsetUniform, offset[0], offset[1]);

  gl.uniform1f(program.scaleUniform, scale);

  // draw it
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

initWebGL();
initShaders();
drawScene();

var iv = null;
var actions = [];
var keyMappings = { '37' : 'panleft', '38' : 'panup', '39' : 'panright', '40' : 'pandown', '187' : 'zoomin', '189' : 'zoomout' };
for (var k in keyMappings) {
  actions[keyMappings[k]] = false;
}

function draw() {
  offset[0] += -(actions.panleft ? scale / 25 : 0) + (actions.panright ? scale / 25 : 0);
  offset[1] += -(actions.pandown ? scale / 25 : 0) + (actions.panup ? scale / 25 : 0);
  scale = scale * (actions.zoomin ? 0.975 : 1.0) / (actions.zoomout ? 0.975 : 1.0);

  gl.uniform2f(program.canvasSizeUniform, canvas.width, canvas.height);
  gl.uniform2f(program.offsetUniform, offset[0], offset[1]);
  gl.uniform1f(program.scaleUniform, scale);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

window.onkeydown = function(e) {
  var kc = e.keyCode.toString();
  if (keyMappings.hasOwnProperty(kc)) {
    actions[keyMappings[kc]] = true;
  }
  if (!iv) {
    iv = setInterval(draw, 40);
  }

};
window.onkeyup = function(e) {
  var kc = e.keyCode.toString();
  if (keyMappings.hasOwnProperty(kc)) {
    actions[keyMappings[kc]] = false;
  }
  clearInterval(iv);
  iv = null;
};

draw();


// error handling
window.onerror = function(msg, url, lineno) {
  alert(url + '(' + lineno + '): ' + msg);
}

</script>

</body>
</html>